@domql/router 3.1.2 → 3.2.7
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/README.md +198 -0
- package/dist/cjs/index.js +140 -12
- package/dist/esm/index.js +154 -38
- package/dist/iife/index.js +312 -0
- package/index.js +175 -12
- package/package.json +26 -17
- package/dist/cjs/package.json +0 -4
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# @domql/router
|
|
2
|
+
|
|
3
|
+
Client-side router plugin for DOMQL. Handles route matching, navigation, scroll management, and state updates within DOMQL elements.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @domql/router
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { router } from '@domql/router'
|
|
15
|
+
|
|
16
|
+
// Navigate to a path
|
|
17
|
+
router('/about', element)
|
|
18
|
+
|
|
19
|
+
// With state and options
|
|
20
|
+
router('/dashboard', element, { userId: 1 }, { scrollToTop: true })
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Define routes on your DOMQL element:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
const App = {
|
|
27
|
+
routes: {
|
|
28
|
+
'/': HomePage,
|
|
29
|
+
'/about': AboutPage,
|
|
30
|
+
'/contact': ContactPage,
|
|
31
|
+
'/*': NotFoundPage
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Dynamic Route Params
|
|
37
|
+
|
|
38
|
+
Match routes with `:param` segments. Enable with `useParamsMatching: true`.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const App = {
|
|
42
|
+
routes: {
|
|
43
|
+
'/': HomePage,
|
|
44
|
+
'/:id': UserPage,
|
|
45
|
+
'/:category/:slug': ArticlePage,
|
|
46
|
+
'/*': NotFoundPage
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
router('/users/42', element, {}, { useParamsMatching: true })
|
|
51
|
+
// state.params = { id: '42' }
|
|
52
|
+
|
|
53
|
+
router('/tech/my-article', element, {}, { useParamsMatching: true })
|
|
54
|
+
// state.params = { category: 'tech', slug: 'my-article' }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Exact segments score higher than params, so `/about` will match a literal `/about` route before `/:id`.
|
|
58
|
+
|
|
59
|
+
## Query String Parsing
|
|
60
|
+
|
|
61
|
+
Query parameters are automatically parsed and stored in state.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
router('/search?q=hello&tag=a&tag=b', element)
|
|
65
|
+
// state.query = { q: 'hello', tag: ['a', 'b'] }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Duplicate keys are collected into arrays.
|
|
69
|
+
|
|
70
|
+
## Guards / Middleware
|
|
71
|
+
|
|
72
|
+
Run async guard functions before navigation. Return `true` to allow, `false` to block, or a string to redirect.
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
const authGuard = ({ element }) => {
|
|
76
|
+
if (!element.state.root.isLoggedIn) return '/login'
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const roleGuard = ({ params }) => {
|
|
81
|
+
if (params.section === 'admin') return false
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
router('/dashboard', element, {}, {
|
|
86
|
+
guards: [authGuard, roleGuard]
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Guard functions receive a context object:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
{
|
|
94
|
+
pathname, // full pathname
|
|
95
|
+
route, // matched route key
|
|
96
|
+
params, // dynamic route params
|
|
97
|
+
query, // parsed query string
|
|
98
|
+
hash, // URL hash
|
|
99
|
+
element, // DOMQL element
|
|
100
|
+
state // navigation state
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 404 Handling
|
|
105
|
+
|
|
106
|
+
Provide an `onNotFound` callback for unmatched routes:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
router('/unknown', element, {}, {
|
|
110
|
+
onNotFound: ({ pathname, route, element }) => {
|
|
111
|
+
console.warn(`No route found for ${pathname}`)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
You can also define a `/*` wildcard route as a catch-all fallback.
|
|
117
|
+
|
|
118
|
+
## Options
|
|
119
|
+
|
|
120
|
+
| Option | Type | Default | Description |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| `level` | `number` | `0` | Route nesting level (which path segment to match) |
|
|
123
|
+
| `pushState` | `boolean` | `true` | Push to browser history |
|
|
124
|
+
| `initialRender` | `boolean` | `false` | Whether this is the initial page render |
|
|
125
|
+
| `scrollToTop` | `boolean` | `true` | Scroll to top after navigation |
|
|
126
|
+
| `scrollToNode` | `boolean` | `false` | Scroll within the element node |
|
|
127
|
+
| `scrollNode` | `Element` | `document.documentElement` | Node to scroll |
|
|
128
|
+
| `scrollToOffset` | `number` | `0` | Offset when scrolling to hash anchors |
|
|
129
|
+
| `scrollToOptions` | `object` | `{ behavior: 'smooth' }` | Options passed to `scrollTo()` |
|
|
130
|
+
| `useFragment` | `boolean` | `false` | Use fragment tag for content |
|
|
131
|
+
| `updateState` | `boolean` | `true` | Update element state on navigation |
|
|
132
|
+
| `contentElementKey` | `string` | `'content'` | Key for the content element slot |
|
|
133
|
+
| `removeOldElement` | `boolean` | `false` | Remove old content element before setting new |
|
|
134
|
+
| `useParamsMatching` | `boolean` | `false` | Enable dynamic `:param` route matching |
|
|
135
|
+
| `guards` | `function[]` | `undefined` | Array of guard/middleware functions |
|
|
136
|
+
| `onNotFound` | `function` | `undefined` | Callback when no route matches |
|
|
137
|
+
|
|
138
|
+
## Exported Utilities
|
|
139
|
+
|
|
140
|
+
### `getActiveRoute(level, route)`
|
|
141
|
+
|
|
142
|
+
Returns the active route segment at the given nesting level.
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
import { getActiveRoute } from '@domql/router'
|
|
146
|
+
|
|
147
|
+
getActiveRoute(0, '/users/42') // '/users'
|
|
148
|
+
getActiveRoute(1, '/users/42') // '/42'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `parseQuery(search)`
|
|
152
|
+
|
|
153
|
+
Parses a query string into an object.
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
import { parseQuery } from '@domql/router'
|
|
157
|
+
|
|
158
|
+
parseQuery('?page=1&sort=name') // { page: '1', sort: 'name' }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `matchRoute(pathname, routes, level)`
|
|
162
|
+
|
|
163
|
+
Matches a pathname against a routes object. Returns `{ key, content, params }`.
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
import { matchRoute } from '@domql/router'
|
|
167
|
+
|
|
168
|
+
const routes = { '/': Home, '/:id': Detail, '/*': NotFound }
|
|
169
|
+
const result = matchRoute('/42', routes)
|
|
170
|
+
// { key: '/:id', content: Detail, params: { id: '42' } }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `parseRoutePattern(pattern)`
|
|
174
|
+
|
|
175
|
+
Parses a route pattern string into segments, param definitions, and wildcard flag. Results are cached.
|
|
176
|
+
|
|
177
|
+
### `runGuards(guards, context)`
|
|
178
|
+
|
|
179
|
+
Runs an array of async guard functions sequentially. Returns `true`, `false`, or a redirect path string.
|
|
180
|
+
|
|
181
|
+
## Events
|
|
182
|
+
|
|
183
|
+
The router triggers an `on.routeChanged` event on the element after navigation completes. Listen for it in your element definition:
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
const App = {
|
|
187
|
+
routes: { ... },
|
|
188
|
+
on: {
|
|
189
|
+
routeChanged: (element, options) => {
|
|
190
|
+
console.log('Route changed:', element.state.route)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
package/dist/cjs/index.js
CHANGED
|
@@ -22,12 +22,115 @@ __export(index_exports, {
|
|
|
22
22
|
getActiveRoute: () => getActiveRoute,
|
|
23
23
|
lastLevel: () => lastLevel,
|
|
24
24
|
lastPathname: () => lastPathname,
|
|
25
|
-
|
|
25
|
+
matchRoute: () => matchRoute,
|
|
26
|
+
parseQuery: () => parseQuery,
|
|
27
|
+
parseRoutePattern: () => parseRoutePattern,
|
|
28
|
+
router: () => router,
|
|
29
|
+
runGuards: () => runGuards
|
|
26
30
|
});
|
|
27
31
|
module.exports = __toCommonJS(index_exports);
|
|
28
|
-
var import_event = require("@domql/event");
|
|
29
32
|
var import_utils = require("@domql/utils");
|
|
30
33
|
var import_set = require("@domql/element/set");
|
|
34
|
+
const paramPattern = /^:(.+)/;
|
|
35
|
+
const wildcardPattern = /^\*$/;
|
|
36
|
+
const routeCache = /* @__PURE__ */ new Map();
|
|
37
|
+
const parseRoutePattern = (pattern) => {
|
|
38
|
+
const cached = routeCache.get(pattern);
|
|
39
|
+
if (cached) return cached;
|
|
40
|
+
const segments = pattern.replace(/^\//, "").split("/");
|
|
41
|
+
const params = [];
|
|
42
|
+
let hasWildcard = false;
|
|
43
|
+
for (let i = 0; i < segments.length; i++) {
|
|
44
|
+
const match = segments[i].match(paramPattern);
|
|
45
|
+
if (match) {
|
|
46
|
+
params.push({ index: i, name: match[1] });
|
|
47
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
48
|
+
hasWildcard = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const result = { segments, params, hasWildcard, pattern };
|
|
52
|
+
routeCache.set(pattern, result);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
const matchRoute = (pathname, routes, level = 0) => {
|
|
56
|
+
const pathSegments = pathname.replace(/^\//, "").split("/").filter(Boolean);
|
|
57
|
+
const relevantSegments = pathSegments.slice(level);
|
|
58
|
+
const routePath = "/" + (relevantSegments[0] || "");
|
|
59
|
+
let bestMatch = null;
|
|
60
|
+
let bestScore = -1;
|
|
61
|
+
let matchedParams = {};
|
|
62
|
+
for (const key in routes) {
|
|
63
|
+
if (key === "/*") continue;
|
|
64
|
+
const parsed = parseRoutePattern(key);
|
|
65
|
+
const score = scoreMatch(relevantSegments, parsed);
|
|
66
|
+
if (score > bestScore) {
|
|
67
|
+
bestScore = score;
|
|
68
|
+
bestMatch = key;
|
|
69
|
+
matchedParams = extractParams(relevantSegments, parsed);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!bestMatch && routes["/*"]) {
|
|
73
|
+
bestMatch = "/*";
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
key: bestMatch,
|
|
77
|
+
content: bestMatch ? routes[bestMatch] : null,
|
|
78
|
+
params: matchedParams,
|
|
79
|
+
routePath
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const scoreMatch = (pathSegments, parsed) => {
|
|
83
|
+
const { segments, hasWildcard } = parsed;
|
|
84
|
+
if (!hasWildcard && segments.length !== pathSegments.length && segments.length !== 1) {
|
|
85
|
+
if (segments.length > pathSegments.length) return -1;
|
|
86
|
+
}
|
|
87
|
+
let score = 0;
|
|
88
|
+
const len = Math.min(segments.length, pathSegments.length);
|
|
89
|
+
for (let i = 0; i < len; i++) {
|
|
90
|
+
if (segments[i] === pathSegments[i]) {
|
|
91
|
+
score += 3;
|
|
92
|
+
} else if (paramPattern.test(segments[i])) {
|
|
93
|
+
score += 1;
|
|
94
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
95
|
+
score += 0.5;
|
|
96
|
+
} else {
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return score;
|
|
101
|
+
};
|
|
102
|
+
const extractParams = (pathSegments, parsed) => {
|
|
103
|
+
const params = {};
|
|
104
|
+
for (const { index, name } of parsed.params) {
|
|
105
|
+
if (pathSegments[index]) {
|
|
106
|
+
params[name] = decodeURIComponent(pathSegments[index]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return params;
|
|
110
|
+
};
|
|
111
|
+
const parseQuery = (search) => {
|
|
112
|
+
if (!search || search === "?") return {};
|
|
113
|
+
const params = {};
|
|
114
|
+
const searchParams = new URLSearchParams(search);
|
|
115
|
+
searchParams.forEach((value, key) => {
|
|
116
|
+
if (params[key] !== void 0) {
|
|
117
|
+
if (!Array.isArray(params[key])) params[key] = [params[key]];
|
|
118
|
+
params[key].push(value);
|
|
119
|
+
} else {
|
|
120
|
+
params[key] = value;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return params;
|
|
124
|
+
};
|
|
125
|
+
const runGuards = async (guards, context) => {
|
|
126
|
+
if (!guards || !guards.length) return true;
|
|
127
|
+
for (const guard of guards) {
|
|
128
|
+
const result = await guard(context);
|
|
129
|
+
if (result === false) return false;
|
|
130
|
+
if (typeof result === "string") return result;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
};
|
|
31
134
|
const getActiveRoute = (level = 0, route = import_utils.window.location.pathname) => {
|
|
32
135
|
const routeArray = route.split("/");
|
|
33
136
|
const activeRoute = routeArray[level + 1];
|
|
@@ -47,9 +150,10 @@ const defaultOptions = {
|
|
|
47
150
|
updateState: true,
|
|
48
151
|
scrollToOffset: 0,
|
|
49
152
|
contentElementKey: "content",
|
|
50
|
-
scrollToOptions: { behavior: "smooth" }
|
|
153
|
+
scrollToOptions: { behavior: "smooth" },
|
|
154
|
+
useParamsMatching: false
|
|
51
155
|
};
|
|
52
|
-
const router =
|
|
156
|
+
const router = (path, el, state = {}, options = {}) => {
|
|
53
157
|
const element = el || void 0;
|
|
54
158
|
const win = element.context.window || import_utils.window;
|
|
55
159
|
const doc = element.context.document || import_utils.document;
|
|
@@ -66,31 +170,55 @@ const router = async (path, el, state = {}, options = {}) => {
|
|
|
66
170
|
const contentElementKey = (0, import_set.setContentKey)(element, opts);
|
|
67
171
|
const urlObj = new win.URL(win.location.origin + path);
|
|
68
172
|
const { pathname, search, hash } = urlObj;
|
|
173
|
+
const query = parseQuery(search);
|
|
69
174
|
const rootNode = element.node;
|
|
70
|
-
const route = getActiveRoute(opts.level, pathname);
|
|
71
|
-
const content = element.routes[route || "/"] || element.routes["/*"];
|
|
72
|
-
const scrollNode = opts.scrollToNode ? rootNode : opts.scrollNode;
|
|
73
175
|
const hashChanged = hash && hash !== win.location.hash.slice(1);
|
|
74
176
|
const pathChanged = pathname !== lastPathname;
|
|
75
177
|
lastPathname = pathname;
|
|
178
|
+
let route, content, params;
|
|
179
|
+
if (opts.useParamsMatching) {
|
|
180
|
+
const match = matchRoute(pathname, element.routes, opts.level);
|
|
181
|
+
route = match.routePath;
|
|
182
|
+
content = match.content;
|
|
183
|
+
params = match.params;
|
|
184
|
+
} else {
|
|
185
|
+
route = getActiveRoute(opts.level, pathname);
|
|
186
|
+
content = element.routes[route || "/"] || element.routes["/*"];
|
|
187
|
+
params = {};
|
|
188
|
+
}
|
|
189
|
+
const scrollNode = opts.scrollToNode ? rootNode : opts.scrollNode;
|
|
76
190
|
if (!content || element.state.root.debugging) {
|
|
77
191
|
element.state.root.debugging = false;
|
|
192
|
+
if (opts.onNotFound) {
|
|
193
|
+
opts.onNotFound({ pathname, route, element });
|
|
194
|
+
}
|
|
78
195
|
return;
|
|
79
196
|
}
|
|
197
|
+
if (opts.guards && opts.guards.length) {
|
|
198
|
+
const guardContext = { pathname, route, params, query, hash, element, state };
|
|
199
|
+
const guardResult = runGuards(opts.guards, guardContext);
|
|
200
|
+
if (guardResult === false) return;
|
|
201
|
+
if (typeof guardResult === "string") {
|
|
202
|
+
return router(guardResult, el, state, { ...options, guards: [] });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
80
205
|
if (opts.pushState) {
|
|
81
206
|
win.history.pushState(state, null, pathname + (search || "") + (hash || ""));
|
|
82
207
|
}
|
|
83
208
|
if (pathChanged || !hashChanged) {
|
|
209
|
+
const stateUpdate = { route, hash, debugging: false };
|
|
210
|
+
if (Object.keys(params).length) stateUpdate.params = params;
|
|
211
|
+
if (Object.keys(query).length) stateUpdate.query = query;
|
|
84
212
|
if (opts.updateState) {
|
|
85
|
-
|
|
86
|
-
|
|
213
|
+
element.state.update(
|
|
214
|
+
stateUpdate,
|
|
87
215
|
{ preventContentUpdate: true }
|
|
88
216
|
);
|
|
89
217
|
}
|
|
90
218
|
if (contentElementKey && opts.removeOldElement) {
|
|
91
219
|
element[contentElementKey].remove();
|
|
92
220
|
}
|
|
93
|
-
|
|
221
|
+
element.set(
|
|
94
222
|
{
|
|
95
223
|
tag: opts.useFragment && "fragment",
|
|
96
224
|
extends: content
|
|
@@ -115,7 +243,7 @@ const router = async (path, el, state = {}, options = {}) => {
|
|
|
115
243
|
if (hash) {
|
|
116
244
|
const activeNode = doc.getElementById(hash);
|
|
117
245
|
if (activeNode) {
|
|
118
|
-
const top = activeNode.getBoundingClientRect().top + rootNode.scrollTop - opts.scrollToOffset || 0;
|
|
246
|
+
const top = activeNode.getBoundingClientRect().top + rootNode.scrollTop - (opts.scrollToOffset || 0);
|
|
119
247
|
scrollNode.scrollTo({
|
|
120
248
|
...opts.scrollToOptions || {},
|
|
121
249
|
top,
|
|
@@ -123,6 +251,6 @@ const router = async (path, el, state = {}, options = {}) => {
|
|
|
123
251
|
});
|
|
124
252
|
}
|
|
125
253
|
}
|
|
126
|
-
|
|
254
|
+
(0, import_utils.triggerEventOn)("routeChanged", element, opts);
|
|
127
255
|
};
|
|
128
256
|
var index_default = router;
|
package/dist/esm/index.js
CHANGED
|
@@ -1,25 +1,105 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { document, window, triggerEventOn } from "@domql/utils";
|
|
2
|
+
import { setContentKey } from "@domql/element/set";
|
|
3
|
+
const paramPattern = /^:(.+)/;
|
|
4
|
+
const wildcardPattern = /^\*$/;
|
|
5
|
+
const routeCache = /* @__PURE__ */ new Map();
|
|
6
|
+
const parseRoutePattern = (pattern) => {
|
|
7
|
+
const cached = routeCache.get(pattern);
|
|
8
|
+
if (cached) return cached;
|
|
9
|
+
const segments = pattern.replace(/^\//, "").split("/");
|
|
10
|
+
const params = [];
|
|
11
|
+
let hasWildcard = false;
|
|
12
|
+
for (let i = 0; i < segments.length; i++) {
|
|
13
|
+
const match = segments[i].match(paramPattern);
|
|
14
|
+
if (match) {
|
|
15
|
+
params.push({ index: i, name: match[1] });
|
|
16
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
17
|
+
hasWildcard = true;
|
|
16
18
|
}
|
|
17
|
-
|
|
19
|
+
}
|
|
20
|
+
const result = { segments, params, hasWildcard, pattern };
|
|
21
|
+
routeCache.set(pattern, result);
|
|
22
|
+
return result;
|
|
23
|
+
};
|
|
24
|
+
const matchRoute = (pathname, routes, level = 0) => {
|
|
25
|
+
const pathSegments = pathname.replace(/^\//, "").split("/").filter(Boolean);
|
|
26
|
+
const relevantSegments = pathSegments.slice(level);
|
|
27
|
+
const routePath = "/" + (relevantSegments[0] || "");
|
|
28
|
+
let bestMatch = null;
|
|
29
|
+
let bestScore = -1;
|
|
30
|
+
let matchedParams = {};
|
|
31
|
+
for (const key in routes) {
|
|
32
|
+
if (key === "/*") continue;
|
|
33
|
+
const parsed = parseRoutePattern(key);
|
|
34
|
+
const score = scoreMatch(relevantSegments, parsed);
|
|
35
|
+
if (score > bestScore) {
|
|
36
|
+
bestScore = score;
|
|
37
|
+
bestMatch = key;
|
|
38
|
+
matchedParams = extractParams(relevantSegments, parsed);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!bestMatch && routes["/*"]) {
|
|
42
|
+
bestMatch = "/*";
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
key: bestMatch,
|
|
46
|
+
content: bestMatch ? routes[bestMatch] : null,
|
|
47
|
+
params: matchedParams,
|
|
48
|
+
routePath
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const scoreMatch = (pathSegments, parsed) => {
|
|
52
|
+
const { segments, hasWildcard } = parsed;
|
|
53
|
+
if (!hasWildcard && segments.length !== pathSegments.length && segments.length !== 1) {
|
|
54
|
+
if (segments.length > pathSegments.length) return -1;
|
|
55
|
+
}
|
|
56
|
+
let score = 0;
|
|
57
|
+
const len = Math.min(segments.length, pathSegments.length);
|
|
58
|
+
for (let i = 0; i < len; i++) {
|
|
59
|
+
if (segments[i] === pathSegments[i]) {
|
|
60
|
+
score += 3;
|
|
61
|
+
} else if (paramPattern.test(segments[i])) {
|
|
62
|
+
score += 1;
|
|
63
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
64
|
+
score += 0.5;
|
|
65
|
+
} else {
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return score;
|
|
70
|
+
};
|
|
71
|
+
const extractParams = (pathSegments, parsed) => {
|
|
72
|
+
const params = {};
|
|
73
|
+
for (const { index, name } of parsed.params) {
|
|
74
|
+
if (pathSegments[index]) {
|
|
75
|
+
params[name] = decodeURIComponent(pathSegments[index]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return params;
|
|
79
|
+
};
|
|
80
|
+
const parseQuery = (search) => {
|
|
81
|
+
if (!search || search === "?") return {};
|
|
82
|
+
const params = {};
|
|
83
|
+
const searchParams = new URLSearchParams(search);
|
|
84
|
+
searchParams.forEach((value, key) => {
|
|
85
|
+
if (params[key] !== void 0) {
|
|
86
|
+
if (!Array.isArray(params[key])) params[key] = [params[key]];
|
|
87
|
+
params[key].push(value);
|
|
88
|
+
} else {
|
|
89
|
+
params[key] = value;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return params;
|
|
93
|
+
};
|
|
94
|
+
const runGuards = async (guards, context) => {
|
|
95
|
+
if (!guards || !guards.length) return true;
|
|
96
|
+
for (const guard of guards) {
|
|
97
|
+
const result = await guard(context);
|
|
98
|
+
if (result === false) return false;
|
|
99
|
+
if (typeof result === "string") return result;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
18
102
|
};
|
|
19
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
-
import { triggerEventOn } from "@domql/event";
|
|
21
|
-
import { document, window } from "@domql/utils";
|
|
22
|
-
import { setContentKey } from "@domql/element/set";
|
|
23
103
|
const getActiveRoute = (level = 0, route = window.location.pathname) => {
|
|
24
104
|
const routeArray = route.split("/");
|
|
25
105
|
const activeRoute = routeArray[level + 1];
|
|
@@ -39,13 +119,18 @@ const defaultOptions = {
|
|
|
39
119
|
updateState: true,
|
|
40
120
|
scrollToOffset: 0,
|
|
41
121
|
contentElementKey: "content",
|
|
42
|
-
scrollToOptions: { behavior: "smooth" }
|
|
122
|
+
scrollToOptions: { behavior: "smooth" },
|
|
123
|
+
useParamsMatching: false
|
|
43
124
|
};
|
|
44
|
-
const router =
|
|
125
|
+
const router = (path, el, state = {}, options = {}) => {
|
|
45
126
|
const element = el || void 0;
|
|
46
127
|
const win = element.context.window || window;
|
|
47
128
|
const doc = element.context.document || document;
|
|
48
|
-
const opts =
|
|
129
|
+
const opts = {
|
|
130
|
+
...defaultOptions,
|
|
131
|
+
...element.context.routerOptions,
|
|
132
|
+
...options
|
|
133
|
+
};
|
|
49
134
|
lastLevel = opts.lastLevel;
|
|
50
135
|
const ref = element.__ref;
|
|
51
136
|
if (opts.contentElementKey !== "content" && opts.contentElementKey !== ref.contentElementKey || !ref.contentElementKey) {
|
|
@@ -54,31 +139,55 @@ const router = async (path, el, state = {}, options = {}) => {
|
|
|
54
139
|
const contentElementKey = setContentKey(element, opts);
|
|
55
140
|
const urlObj = new win.URL(win.location.origin + path);
|
|
56
141
|
const { pathname, search, hash } = urlObj;
|
|
142
|
+
const query = parseQuery(search);
|
|
57
143
|
const rootNode = element.node;
|
|
58
|
-
const route = getActiveRoute(opts.level, pathname);
|
|
59
|
-
const content = element.routes[route || "/"] || element.routes["/*"];
|
|
60
|
-
const scrollNode = opts.scrollToNode ? rootNode : opts.scrollNode;
|
|
61
144
|
const hashChanged = hash && hash !== win.location.hash.slice(1);
|
|
62
145
|
const pathChanged = pathname !== lastPathname;
|
|
63
146
|
lastPathname = pathname;
|
|
147
|
+
let route, content, params;
|
|
148
|
+
if (opts.useParamsMatching) {
|
|
149
|
+
const match = matchRoute(pathname, element.routes, opts.level);
|
|
150
|
+
route = match.routePath;
|
|
151
|
+
content = match.content;
|
|
152
|
+
params = match.params;
|
|
153
|
+
} else {
|
|
154
|
+
route = getActiveRoute(opts.level, pathname);
|
|
155
|
+
content = element.routes[route || "/"] || element.routes["/*"];
|
|
156
|
+
params = {};
|
|
157
|
+
}
|
|
158
|
+
const scrollNode = opts.scrollToNode ? rootNode : opts.scrollNode;
|
|
64
159
|
if (!content || element.state.root.debugging) {
|
|
65
160
|
element.state.root.debugging = false;
|
|
161
|
+
if (opts.onNotFound) {
|
|
162
|
+
opts.onNotFound({ pathname, route, element });
|
|
163
|
+
}
|
|
66
164
|
return;
|
|
67
165
|
}
|
|
166
|
+
if (opts.guards && opts.guards.length) {
|
|
167
|
+
const guardContext = { pathname, route, params, query, hash, element, state };
|
|
168
|
+
const guardResult = runGuards(opts.guards, guardContext);
|
|
169
|
+
if (guardResult === false) return;
|
|
170
|
+
if (typeof guardResult === "string") {
|
|
171
|
+
return router(guardResult, el, state, { ...options, guards: [] });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
68
174
|
if (opts.pushState) {
|
|
69
175
|
win.history.pushState(state, null, pathname + (search || "") + (hash || ""));
|
|
70
176
|
}
|
|
71
177
|
if (pathChanged || !hashChanged) {
|
|
178
|
+
const stateUpdate = { route, hash, debugging: false };
|
|
179
|
+
if (Object.keys(params).length) stateUpdate.params = params;
|
|
180
|
+
if (Object.keys(query).length) stateUpdate.query = query;
|
|
72
181
|
if (opts.updateState) {
|
|
73
|
-
|
|
74
|
-
|
|
182
|
+
element.state.update(
|
|
183
|
+
stateUpdate,
|
|
75
184
|
{ preventContentUpdate: true }
|
|
76
185
|
);
|
|
77
186
|
}
|
|
78
187
|
if (contentElementKey && opts.removeOldElement) {
|
|
79
188
|
element[contentElementKey].remove();
|
|
80
189
|
}
|
|
81
|
-
|
|
190
|
+
element.set(
|
|
82
191
|
{
|
|
83
192
|
tag: opts.useFragment && "fragment",
|
|
84
193
|
extends: content
|
|
@@ -87,28 +196,31 @@ const router = async (path, el, state = {}, options = {}) => {
|
|
|
87
196
|
);
|
|
88
197
|
}
|
|
89
198
|
if (opts.scrollToTop) {
|
|
90
|
-
scrollNode.scrollTo(
|
|
199
|
+
scrollNode.scrollTo({
|
|
200
|
+
...opts.scrollToOptions || {},
|
|
91
201
|
top: 0,
|
|
92
202
|
left: 0
|
|
93
|
-
})
|
|
203
|
+
});
|
|
94
204
|
}
|
|
95
205
|
if (opts.scrollToNode) {
|
|
96
|
-
content[contentElementKey].node.scrollTo(
|
|
206
|
+
content[contentElementKey].node.scrollTo({
|
|
207
|
+
...opts.scrollToOptions || {},
|
|
97
208
|
top: 0,
|
|
98
209
|
left: 0
|
|
99
|
-
})
|
|
210
|
+
});
|
|
100
211
|
}
|
|
101
212
|
if (hash) {
|
|
102
213
|
const activeNode = doc.getElementById(hash);
|
|
103
214
|
if (activeNode) {
|
|
104
|
-
const top = activeNode.getBoundingClientRect().top + rootNode.scrollTop - opts.scrollToOffset || 0;
|
|
105
|
-
scrollNode.scrollTo(
|
|
215
|
+
const top = activeNode.getBoundingClientRect().top + rootNode.scrollTop - (opts.scrollToOffset || 0);
|
|
216
|
+
scrollNode.scrollTo({
|
|
217
|
+
...opts.scrollToOptions || {},
|
|
106
218
|
top,
|
|
107
219
|
left: 0
|
|
108
|
-
})
|
|
220
|
+
});
|
|
109
221
|
}
|
|
110
222
|
}
|
|
111
|
-
|
|
223
|
+
triggerEventOn("routeChanged", element, opts);
|
|
112
224
|
};
|
|
113
225
|
var index_default = router;
|
|
114
226
|
export {
|
|
@@ -116,5 +228,9 @@ export {
|
|
|
116
228
|
getActiveRoute,
|
|
117
229
|
lastLevel,
|
|
118
230
|
lastPathname,
|
|
119
|
-
|
|
231
|
+
matchRoute,
|
|
232
|
+
parseQuery,
|
|
233
|
+
parseRoutePattern,
|
|
234
|
+
router,
|
|
235
|
+
runGuards
|
|
120
236
|
};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var DomqlRouter = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// index.js
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
default: () => index_default,
|
|
25
|
+
getActiveRoute: () => getActiveRoute,
|
|
26
|
+
lastLevel: () => lastLevel,
|
|
27
|
+
lastPathname: () => lastPathname,
|
|
28
|
+
matchRoute: () => matchRoute,
|
|
29
|
+
parseQuery: () => parseQuery,
|
|
30
|
+
parseRoutePattern: () => parseRoutePattern,
|
|
31
|
+
router: () => router,
|
|
32
|
+
runGuards: () => runGuards
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ../../packages/utils/dist/esm/globals.js
|
|
36
|
+
var window2 = globalThis;
|
|
37
|
+
var document = window2.document;
|
|
38
|
+
|
|
39
|
+
// ../../packages/utils/dist/esm/types.js
|
|
40
|
+
var isFunction = (arg) => typeof arg === "function";
|
|
41
|
+
|
|
42
|
+
// ../../packages/utils/dist/esm/triggerEvent.js
|
|
43
|
+
var getOnOrPropsEvent = (param, element) => {
|
|
44
|
+
const onEvent = element.on?.[param];
|
|
45
|
+
if (onEvent) return onEvent;
|
|
46
|
+
const props = element.props;
|
|
47
|
+
if (!props) return;
|
|
48
|
+
const propKey = "on" + param.charAt(0).toUpperCase() + param.slice(1);
|
|
49
|
+
return props[propKey];
|
|
50
|
+
};
|
|
51
|
+
var applyEvent = (param, element, state, context, options) => {
|
|
52
|
+
if (!isFunction(param)) return;
|
|
53
|
+
const result = param.call(
|
|
54
|
+
element,
|
|
55
|
+
element,
|
|
56
|
+
state || element.state,
|
|
57
|
+
context || element.context,
|
|
58
|
+
options
|
|
59
|
+
);
|
|
60
|
+
if (result && typeof result.then === "function") {
|
|
61
|
+
result.catch(() => {
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
var triggerEventOn = (param, element, options) => {
|
|
67
|
+
if (!element) {
|
|
68
|
+
throw new Error("Element is required");
|
|
69
|
+
}
|
|
70
|
+
const appliedFunction = getOnOrPropsEvent(param, element);
|
|
71
|
+
if (appliedFunction) {
|
|
72
|
+
const { state, context } = element;
|
|
73
|
+
return applyEvent(appliedFunction, element, state, context, options);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ../../packages/element/dist/esm/set.js
|
|
78
|
+
var setContentKey = (element, opts = {}) => {
|
|
79
|
+
const { __ref: ref } = element;
|
|
80
|
+
const contentElementKey = opts.contentElementKey;
|
|
81
|
+
if (!ref.contentElementKey || contentElementKey !== ref.contentElementKey) {
|
|
82
|
+
ref.contentElementKey = contentElementKey || "content";
|
|
83
|
+
}
|
|
84
|
+
return ref.contentElementKey;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// index.js
|
|
88
|
+
var paramPattern = /^:(.+)/;
|
|
89
|
+
var wildcardPattern = /^\*$/;
|
|
90
|
+
var routeCache = /* @__PURE__ */ new Map();
|
|
91
|
+
var parseRoutePattern = (pattern) => {
|
|
92
|
+
const cached = routeCache.get(pattern);
|
|
93
|
+
if (cached) return cached;
|
|
94
|
+
const segments = pattern.replace(/^\//, "").split("/");
|
|
95
|
+
const params = [];
|
|
96
|
+
let hasWildcard = false;
|
|
97
|
+
for (let i = 0; i < segments.length; i++) {
|
|
98
|
+
const match = segments[i].match(paramPattern);
|
|
99
|
+
if (match) {
|
|
100
|
+
params.push({ index: i, name: match[1] });
|
|
101
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
102
|
+
hasWildcard = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const result = { segments, params, hasWildcard, pattern };
|
|
106
|
+
routeCache.set(pattern, result);
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
var matchRoute = (pathname, routes, level = 0) => {
|
|
110
|
+
const pathSegments = pathname.replace(/^\//, "").split("/").filter(Boolean);
|
|
111
|
+
const relevantSegments = pathSegments.slice(level);
|
|
112
|
+
const routePath = "/" + (relevantSegments[0] || "");
|
|
113
|
+
let bestMatch = null;
|
|
114
|
+
let bestScore = -1;
|
|
115
|
+
let matchedParams = {};
|
|
116
|
+
for (const key in routes) {
|
|
117
|
+
if (key === "/*") continue;
|
|
118
|
+
const parsed = parseRoutePattern(key);
|
|
119
|
+
const score = scoreMatch(relevantSegments, parsed);
|
|
120
|
+
if (score > bestScore) {
|
|
121
|
+
bestScore = score;
|
|
122
|
+
bestMatch = key;
|
|
123
|
+
matchedParams = extractParams(relevantSegments, parsed);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!bestMatch && routes["/*"]) {
|
|
127
|
+
bestMatch = "/*";
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
key: bestMatch,
|
|
131
|
+
content: bestMatch ? routes[bestMatch] : null,
|
|
132
|
+
params: matchedParams,
|
|
133
|
+
routePath
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
var scoreMatch = (pathSegments, parsed) => {
|
|
137
|
+
const { segments, hasWildcard } = parsed;
|
|
138
|
+
if (!hasWildcard && segments.length !== pathSegments.length && segments.length !== 1) {
|
|
139
|
+
if (segments.length > pathSegments.length) return -1;
|
|
140
|
+
}
|
|
141
|
+
let score = 0;
|
|
142
|
+
const len = Math.min(segments.length, pathSegments.length);
|
|
143
|
+
for (let i = 0; i < len; i++) {
|
|
144
|
+
if (segments[i] === pathSegments[i]) {
|
|
145
|
+
score += 3;
|
|
146
|
+
} else if (paramPattern.test(segments[i])) {
|
|
147
|
+
score += 1;
|
|
148
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
149
|
+
score += 0.5;
|
|
150
|
+
} else {
|
|
151
|
+
return -1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return score;
|
|
155
|
+
};
|
|
156
|
+
var extractParams = (pathSegments, parsed) => {
|
|
157
|
+
const params = {};
|
|
158
|
+
for (const { index, name } of parsed.params) {
|
|
159
|
+
if (pathSegments[index]) {
|
|
160
|
+
params[name] = decodeURIComponent(pathSegments[index]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return params;
|
|
164
|
+
};
|
|
165
|
+
var parseQuery = (search) => {
|
|
166
|
+
if (!search || search === "?") return {};
|
|
167
|
+
const params = {};
|
|
168
|
+
const searchParams = new URLSearchParams(search);
|
|
169
|
+
searchParams.forEach((value, key) => {
|
|
170
|
+
if (params[key] !== void 0) {
|
|
171
|
+
if (!Array.isArray(params[key])) params[key] = [params[key]];
|
|
172
|
+
params[key].push(value);
|
|
173
|
+
} else {
|
|
174
|
+
params[key] = value;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return params;
|
|
178
|
+
};
|
|
179
|
+
var runGuards = async (guards, context) => {
|
|
180
|
+
if (!guards || !guards.length) return true;
|
|
181
|
+
for (const guard of guards) {
|
|
182
|
+
const result = await guard(context);
|
|
183
|
+
if (result === false) return false;
|
|
184
|
+
if (typeof result === "string") return result;
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
};
|
|
188
|
+
var getActiveRoute = (level = 0, route = window2.location.pathname) => {
|
|
189
|
+
const routeArray = route.split("/");
|
|
190
|
+
const activeRoute = routeArray[level + 1];
|
|
191
|
+
if (activeRoute) return `/${activeRoute}`;
|
|
192
|
+
};
|
|
193
|
+
var lastPathname;
|
|
194
|
+
var lastLevel = 0;
|
|
195
|
+
var defaultOptions = {
|
|
196
|
+
level: lastLevel,
|
|
197
|
+
pushState: true,
|
|
198
|
+
initialRender: false,
|
|
199
|
+
scrollToTop: true,
|
|
200
|
+
scrollToNode: false,
|
|
201
|
+
scrollNode: document && document.documentElement,
|
|
202
|
+
scrollBody: false,
|
|
203
|
+
useFragment: false,
|
|
204
|
+
updateState: true,
|
|
205
|
+
scrollToOffset: 0,
|
|
206
|
+
contentElementKey: "content",
|
|
207
|
+
scrollToOptions: { behavior: "smooth" },
|
|
208
|
+
useParamsMatching: false
|
|
209
|
+
};
|
|
210
|
+
var router = (path, el, state = {}, options = {}) => {
|
|
211
|
+
const element = el || void 0;
|
|
212
|
+
const win = element.context.window || window2;
|
|
213
|
+
const doc = element.context.document || document;
|
|
214
|
+
const opts = {
|
|
215
|
+
...defaultOptions,
|
|
216
|
+
...element.context.routerOptions,
|
|
217
|
+
...options
|
|
218
|
+
};
|
|
219
|
+
lastLevel = opts.lastLevel;
|
|
220
|
+
const ref = element.__ref;
|
|
221
|
+
if (opts.contentElementKey !== "content" && opts.contentElementKey !== ref.contentElementKey || !ref.contentElementKey) {
|
|
222
|
+
ref.contentElementKey = opts.contentElementKey || "content";
|
|
223
|
+
}
|
|
224
|
+
const contentElementKey = setContentKey(element, opts);
|
|
225
|
+
const urlObj = new win.URL(win.location.origin + path);
|
|
226
|
+
const { pathname, search, hash } = urlObj;
|
|
227
|
+
const query = parseQuery(search);
|
|
228
|
+
const rootNode = element.node;
|
|
229
|
+
const hashChanged = hash && hash !== win.location.hash.slice(1);
|
|
230
|
+
const pathChanged = pathname !== lastPathname;
|
|
231
|
+
lastPathname = pathname;
|
|
232
|
+
let route, content, params;
|
|
233
|
+
if (opts.useParamsMatching) {
|
|
234
|
+
const match = matchRoute(pathname, element.routes, opts.level);
|
|
235
|
+
route = match.routePath;
|
|
236
|
+
content = match.content;
|
|
237
|
+
params = match.params;
|
|
238
|
+
} else {
|
|
239
|
+
route = getActiveRoute(opts.level, pathname);
|
|
240
|
+
content = element.routes[route || "/"] || element.routes["/*"];
|
|
241
|
+
params = {};
|
|
242
|
+
}
|
|
243
|
+
const scrollNode = opts.scrollToNode ? rootNode : opts.scrollNode;
|
|
244
|
+
if (!content || element.state.root.debugging) {
|
|
245
|
+
element.state.root.debugging = false;
|
|
246
|
+
if (opts.onNotFound) {
|
|
247
|
+
opts.onNotFound({ pathname, route, element });
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (opts.guards && opts.guards.length) {
|
|
252
|
+
const guardContext = { pathname, route, params, query, hash, element, state };
|
|
253
|
+
const guardResult = runGuards(opts.guards, guardContext);
|
|
254
|
+
if (guardResult === false) return;
|
|
255
|
+
if (typeof guardResult === "string") {
|
|
256
|
+
return router(guardResult, el, state, { ...options, guards: [] });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (opts.pushState) {
|
|
260
|
+
win.history.pushState(state, null, pathname + (search || "") + (hash || ""));
|
|
261
|
+
}
|
|
262
|
+
if (pathChanged || !hashChanged) {
|
|
263
|
+
const stateUpdate = { route, hash, debugging: false };
|
|
264
|
+
if (Object.keys(params).length) stateUpdate.params = params;
|
|
265
|
+
if (Object.keys(query).length) stateUpdate.query = query;
|
|
266
|
+
if (opts.updateState) {
|
|
267
|
+
element.state.update(
|
|
268
|
+
stateUpdate,
|
|
269
|
+
{ preventContentUpdate: true }
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (contentElementKey && opts.removeOldElement) {
|
|
273
|
+
element[contentElementKey].remove();
|
|
274
|
+
}
|
|
275
|
+
element.set(
|
|
276
|
+
{
|
|
277
|
+
tag: opts.useFragment && "fragment",
|
|
278
|
+
extends: content
|
|
279
|
+
},
|
|
280
|
+
{ contentElementKey }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (opts.scrollToTop) {
|
|
284
|
+
scrollNode.scrollTo({
|
|
285
|
+
...opts.scrollToOptions || {},
|
|
286
|
+
top: 0,
|
|
287
|
+
left: 0
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (opts.scrollToNode) {
|
|
291
|
+
content[contentElementKey].node.scrollTo({
|
|
292
|
+
...opts.scrollToOptions || {},
|
|
293
|
+
top: 0,
|
|
294
|
+
left: 0
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (hash) {
|
|
298
|
+
const activeNode = doc.getElementById(hash);
|
|
299
|
+
if (activeNode) {
|
|
300
|
+
const top = activeNode.getBoundingClientRect().top + rootNode.scrollTop - (opts.scrollToOffset || 0);
|
|
301
|
+
scrollNode.scrollTo({
|
|
302
|
+
...opts.scrollToOptions || {},
|
|
303
|
+
top,
|
|
304
|
+
left: 0
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
triggerEventOn("routeChanged", element, opts);
|
|
309
|
+
};
|
|
310
|
+
var index_default = router;
|
|
311
|
+
return __toCommonJS(index_exports);
|
|
312
|
+
})();
|
package/index.js
CHANGED
|
@@ -1,9 +1,137 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
import { triggerEventOn } from '@domql/
|
|
4
|
-
import { document, window } from '@domql/utils'
|
|
3
|
+
import { document, window, triggerEventOn } from '@domql/utils'
|
|
5
4
|
import { setContentKey } from '@domql/element/set'
|
|
6
5
|
|
|
6
|
+
// --- Route matching utilities ---
|
|
7
|
+
|
|
8
|
+
const paramPattern = /^:(.+)/
|
|
9
|
+
const wildcardPattern = /^\*$/
|
|
10
|
+
|
|
11
|
+
const routeCache = new Map()
|
|
12
|
+
|
|
13
|
+
export const parseRoutePattern = (pattern) => {
|
|
14
|
+
const cached = routeCache.get(pattern)
|
|
15
|
+
if (cached) return cached
|
|
16
|
+
|
|
17
|
+
const segments = pattern.replace(/^\//, '').split('/')
|
|
18
|
+
const params = []
|
|
19
|
+
let hasWildcard = false
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < segments.length; i++) {
|
|
22
|
+
const match = segments[i].match(paramPattern)
|
|
23
|
+
if (match) {
|
|
24
|
+
params.push({ index: i, name: match[1] })
|
|
25
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
26
|
+
hasWildcard = true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = { segments, params, hasWildcard, pattern }
|
|
31
|
+
routeCache.set(pattern, result)
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const matchRoute = (pathname, routes, level = 0) => {
|
|
36
|
+
const pathSegments = pathname.replace(/^\//, '').split('/').filter(Boolean)
|
|
37
|
+
const relevantSegments = pathSegments.slice(level)
|
|
38
|
+
const routePath = '/' + (relevantSegments[0] || '')
|
|
39
|
+
|
|
40
|
+
let bestMatch = null
|
|
41
|
+
let bestScore = -1
|
|
42
|
+
let matchedParams = {}
|
|
43
|
+
|
|
44
|
+
for (const key in routes) {
|
|
45
|
+
if (key === '/*') continue
|
|
46
|
+
|
|
47
|
+
const parsed = parseRoutePattern(key)
|
|
48
|
+
const score = scoreMatch(relevantSegments, parsed)
|
|
49
|
+
|
|
50
|
+
if (score > bestScore) {
|
|
51
|
+
bestScore = score
|
|
52
|
+
bestMatch = key
|
|
53
|
+
matchedParams = extractParams(relevantSegments, parsed)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!bestMatch && routes['/*']) {
|
|
58
|
+
bestMatch = '/*'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
key: bestMatch,
|
|
63
|
+
content: bestMatch ? routes[bestMatch] : null,
|
|
64
|
+
params: matchedParams,
|
|
65
|
+
routePath
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const scoreMatch = (pathSegments, parsed) => {
|
|
70
|
+
const { segments, hasWildcard } = parsed
|
|
71
|
+
|
|
72
|
+
if (!hasWildcard && segments.length !== pathSegments.length &&
|
|
73
|
+
segments.length !== 1) {
|
|
74
|
+
// For single-segment patterns, match just the first segment
|
|
75
|
+
if (segments.length > pathSegments.length) return -1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let score = 0
|
|
79
|
+
const len = Math.min(segments.length, pathSegments.length)
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < len; i++) {
|
|
82
|
+
if (segments[i] === pathSegments[i]) {
|
|
83
|
+
score += 3 // exact match
|
|
84
|
+
} else if (paramPattern.test(segments[i])) {
|
|
85
|
+
score += 1 // param match
|
|
86
|
+
} else if (wildcardPattern.test(segments[i])) {
|
|
87
|
+
score += 0.5
|
|
88
|
+
} else {
|
|
89
|
+
return -1 // no match
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return score
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const extractParams = (pathSegments, parsed) => {
|
|
97
|
+
const params = {}
|
|
98
|
+
for (const { index, name } of parsed.params) {
|
|
99
|
+
if (pathSegments[index]) {
|
|
100
|
+
params[name] = decodeURIComponent(pathSegments[index])
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return params
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const parseQuery = (search) => {
|
|
107
|
+
if (!search || search === '?') return {}
|
|
108
|
+
const params = {}
|
|
109
|
+
const searchParams = new URLSearchParams(search)
|
|
110
|
+
searchParams.forEach((value, key) => {
|
|
111
|
+
if (params[key] !== undefined) {
|
|
112
|
+
if (!Array.isArray(params[key])) params[key] = [params[key]]
|
|
113
|
+
params[key].push(value)
|
|
114
|
+
} else {
|
|
115
|
+
params[key] = value
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
return params
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Route guards / middleware ---
|
|
122
|
+
|
|
123
|
+
export const runGuards = async (guards, context) => {
|
|
124
|
+
if (!guards || !guards.length) return true
|
|
125
|
+
for (const guard of guards) {
|
|
126
|
+
const result = await guard(context)
|
|
127
|
+
if (result === false) return false
|
|
128
|
+
if (typeof result === 'string') return result // redirect path
|
|
129
|
+
}
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Core router ---
|
|
134
|
+
|
|
7
135
|
export const getActiveRoute = (level = 0, route = window.location.pathname) => {
|
|
8
136
|
const routeArray = route.split('/')
|
|
9
137
|
const activeRoute = routeArray[level + 1]
|
|
@@ -25,10 +153,11 @@ const defaultOptions = {
|
|
|
25
153
|
updateState: true,
|
|
26
154
|
scrollToOffset: 0,
|
|
27
155
|
contentElementKey: 'content',
|
|
28
|
-
scrollToOptions: { behavior: 'smooth' }
|
|
156
|
+
scrollToOptions: { behavior: 'smooth' },
|
|
157
|
+
useParamsMatching: false
|
|
29
158
|
}
|
|
30
159
|
|
|
31
|
-
export const router =
|
|
160
|
+
export const router = (path, el, state = {}, options = {}) => {
|
|
32
161
|
const element = el || this
|
|
33
162
|
const win = element.context.window || window
|
|
34
163
|
const doc = element.context.document || document
|
|
@@ -38,6 +167,7 @@ export const router = async (path, el, state = {}, options = {}) => {
|
|
|
38
167
|
...options
|
|
39
168
|
}
|
|
40
169
|
lastLevel = opts.lastLevel
|
|
170
|
+
|
|
41
171
|
const ref = element.__ref
|
|
42
172
|
|
|
43
173
|
if (
|
|
@@ -53,27 +183,60 @@ export const router = async (path, el, state = {}, options = {}) => {
|
|
|
53
183
|
const urlObj = new win.URL(win.location.origin + path)
|
|
54
184
|
const { pathname, search, hash } = urlObj
|
|
55
185
|
|
|
186
|
+
const query = parseQuery(search)
|
|
187
|
+
|
|
56
188
|
const rootNode = element.node
|
|
57
|
-
const route = getActiveRoute(opts.level, pathname)
|
|
58
|
-
const content = element.routes[route || '/'] || element.routes['/*']
|
|
59
|
-
const scrollNode = opts.scrollToNode ? rootNode : opts.scrollNode
|
|
60
189
|
const hashChanged = hash && hash !== win.location.hash.slice(1)
|
|
61
190
|
const pathChanged = pathname !== lastPathname
|
|
62
191
|
lastPathname = pathname
|
|
63
192
|
|
|
193
|
+
// Route matching - support both simple and param-based
|
|
194
|
+
let route, content, params
|
|
195
|
+
if (opts.useParamsMatching) {
|
|
196
|
+
const match = matchRoute(pathname, element.routes, opts.level)
|
|
197
|
+
route = match.routePath
|
|
198
|
+
content = match.content
|
|
199
|
+
params = match.params
|
|
200
|
+
} else {
|
|
201
|
+
route = getActiveRoute(opts.level, pathname)
|
|
202
|
+
content = element.routes[route || '/'] || element.routes['/*']
|
|
203
|
+
params = {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const scrollNode = opts.scrollToNode ? rootNode : opts.scrollNode
|
|
207
|
+
|
|
64
208
|
if (!content || element.state.root.debugging) {
|
|
65
209
|
element.state.root.debugging = false
|
|
210
|
+
|
|
211
|
+
if (opts.onNotFound) {
|
|
212
|
+
opts.onNotFound({ pathname, route, element })
|
|
213
|
+
}
|
|
66
214
|
return
|
|
67
215
|
}
|
|
68
216
|
|
|
217
|
+
// Run guards
|
|
218
|
+
if (opts.guards && opts.guards.length) {
|
|
219
|
+
const guardContext = { pathname, route, params, query, hash, element, state }
|
|
220
|
+
const guardResult = runGuards(opts.guards, guardContext)
|
|
221
|
+
if (guardResult === false) return
|
|
222
|
+
if (typeof guardResult === 'string') {
|
|
223
|
+
// Redirect
|
|
224
|
+
return router(guardResult, el, state, { ...options, guards: [] })
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
69
228
|
if (opts.pushState) {
|
|
70
229
|
win.history.pushState(state, null, pathname + (search || '') + (hash || ''))
|
|
71
230
|
}
|
|
72
231
|
|
|
73
232
|
if (pathChanged || !hashChanged) {
|
|
233
|
+
const stateUpdate = { route, hash, debugging: false }
|
|
234
|
+
if (Object.keys(params).length) stateUpdate.params = params
|
|
235
|
+
if (Object.keys(query).length) stateUpdate.query = query
|
|
236
|
+
|
|
74
237
|
if (opts.updateState) {
|
|
75
|
-
|
|
76
|
-
|
|
238
|
+
element.state.update(
|
|
239
|
+
stateUpdate,
|
|
77
240
|
{ preventContentUpdate: true }
|
|
78
241
|
)
|
|
79
242
|
}
|
|
@@ -82,7 +245,7 @@ export const router = async (path, el, state = {}, options = {}) => {
|
|
|
82
245
|
element[contentElementKey].remove()
|
|
83
246
|
}
|
|
84
247
|
|
|
85
|
-
|
|
248
|
+
element.set(
|
|
86
249
|
{
|
|
87
250
|
tag: opts.useFragment && 'fragment',
|
|
88
251
|
extends: content
|
|
@@ -112,7 +275,7 @@ export const router = async (path, el, state = {}, options = {}) => {
|
|
|
112
275
|
const top =
|
|
113
276
|
activeNode.getBoundingClientRect().top +
|
|
114
277
|
rootNode.scrollTop -
|
|
115
|
-
opts.scrollToOffset || 0
|
|
278
|
+
(opts.scrollToOffset || 0)
|
|
116
279
|
scrollNode.scrollTo({
|
|
117
280
|
...(opts.scrollToOptions || {}),
|
|
118
281
|
top,
|
|
@@ -122,7 +285,7 @@ export const router = async (path, el, state = {}, options = {}) => {
|
|
|
122
285
|
}
|
|
123
286
|
|
|
124
287
|
// trigger `on.routeChanged`
|
|
125
|
-
|
|
288
|
+
triggerEventOn('routeChanged', element, opts)
|
|
126
289
|
}
|
|
127
290
|
|
|
128
291
|
export default router
|
package/package.json
CHANGED
|
@@ -1,32 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@domql/router",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"module": "dist/esm/index.js",
|
|
7
|
-
"unpkg": "dist/iife/index.js",
|
|
8
|
-
"jsdelivr": "dist/iife/index.js",
|
|
9
|
-
"main": "dist/
|
|
10
|
-
"exports":
|
|
6
|
+
"module": "./dist/esm/index.js",
|
|
7
|
+
"unpkg": "./dist/iife/index.js",
|
|
8
|
+
"jsdelivr": "./dist/iife/index.js",
|
|
9
|
+
"main": "./dist/cjs/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/esm/index.js",
|
|
13
|
+
"require": "./dist/cjs/index.js",
|
|
14
|
+
"browser": "./dist/iife/index.js",
|
|
15
|
+
"default": "./dist/esm/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
11
18
|
"source": "index.js",
|
|
12
19
|
"files": [
|
|
13
|
-
"
|
|
14
|
-
"
|
|
20
|
+
"dist",
|
|
21
|
+
"*.js"
|
|
15
22
|
],
|
|
16
23
|
"scripts": {
|
|
17
24
|
"copy:package:cjs": "cp ../../build/package-cjs.json dist/cjs/package.json",
|
|
18
|
-
"build:esm": "
|
|
19
|
-
"build:cjs": "
|
|
20
|
-
"build:iife": "
|
|
21
|
-
"build": "
|
|
22
|
-
"prepublish": "
|
|
25
|
+
"build:esm": "cross-env NODE_ENV=$NODE_ENV esbuild *.js --target=es2020 --format=esm --outdir=dist/esm --define:process.env.NODE_ENV=process.env.NODE_ENV",
|
|
26
|
+
"build:cjs": "cross-env NODE_ENV=$NODE_ENV esbuild *.js --target=node18 --format=cjs --outdir=dist/cjs --define:process.env.NODE_ENV=process.env.NODE_ENV",
|
|
27
|
+
"build:iife": "cross-env NODE_ENV=$NODE_ENV esbuild index.js --bundle --target=es2020 --format=iife --global-name=DomqlRouter --outfile=dist/iife/index.js --define:process.env.NODE_ENV=process.env.NODE_ENV",
|
|
28
|
+
"build": "node ../../build/build.js",
|
|
29
|
+
"prepublish": "npm run build && npm run copy:package:cjs"
|
|
23
30
|
},
|
|
24
31
|
"dependencies": {
|
|
25
|
-
"@domql/
|
|
26
|
-
"@domql/utils": "^3.
|
|
32
|
+
"@domql/element": "^3.2.3",
|
|
33
|
+
"@domql/utils": "^3.2.3"
|
|
27
34
|
},
|
|
28
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "9fc1b79b41cdc725ca6b24aec64920a599634681",
|
|
29
36
|
"devDependencies": {
|
|
30
37
|
"@babel/core": "^7.26.0"
|
|
31
|
-
}
|
|
38
|
+
},
|
|
39
|
+
"browser": "./dist/iife/index.js",
|
|
40
|
+
"sideEffects": false
|
|
32
41
|
}
|
package/dist/cjs/package.json
DELETED