breeze-router 0.2.1 → 0.4.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/README.md CHANGED
@@ -1,35 +1,58 @@
1
1
  # Breeze Router
2
+
2
3
  A lightweight, zero-dependency client-side router for single page applications (SPAs).
3
4
 
4
- **Note: This project is not production ready and is still in development.**
5
5
  ## Installation
6
6
 
7
7
  To use this router in your project, install the router using npm:
8
8
 
9
+ ### 1. Through NPM
10
+
9
11
  ```bash
10
12
  npm install breeze-router
11
13
  ```
12
14
 
15
+ ### 2. Through CDN link
16
+
17
+ ```javascript
18
+ <script type="module">
19
+ import BreezeRouter from
20
+ "https://unpkg.com/breeze-router@0.4.0/dist/BreezeRouter.min.js";
21
+ </script>
22
+ ```
23
+
13
24
  ## Usage
14
25
 
15
26
  To use the router in your application, you need to import `BreezeRouter` and define routes and handlers using the `Router` class:
16
27
 
17
28
  ```javascript
18
- import BeezeRouter from 'breeze-router';
29
+ // NPM.
30
+ import BeezeRouter from "breeze-router";
31
+
32
+ // CDN.
33
+ import BreezeRouter from "https://unpkg.com/breeze-router@0.4.0/dist/BreezeRouter.min.js";
19
34
 
20
35
  // Create a new `BreezeRouter` instance.
21
36
  const ROUTER = new BreezeRouter();
22
37
 
23
38
  // Define routes using the `add()` method.
24
- ROUTER.add('/', async () => {
39
+ ROUTER.add("/", async () => {
25
40
  // Handle the root route
26
41
  });
27
42
 
28
- ROUTER.add('/about', async () => {
29
- // Handle the about route
43
+ ROUTER.add("/about", async ({ route, params }) => {
44
+ // Handle the about route.
45
+ // route.path equals to current route with a trailing splash, e.g.: /about/
46
+ });
47
+
48
+ ROUTER.add("/users/:userId", async ({ route, params }) => {
49
+ // Handle the users route with a dynamic parameter :userId
50
+ const userId = params.userId;
30
51
  });
31
52
 
32
- ROUTER.add('/users/:userId', async ({ route, params }) => {
53
+ // You can define a type for the placeholder param to
54
+ // avoid conflicts with other similar route like "/users/history".
55
+ ROUTER.add("/users/:userId<number>", async ({ route, params }) => {
33
56
  // Handle the users route with a dynamic parameter :userId
34
57
  const userId = params.userId;
35
58
  });
@@ -43,16 +66,6 @@ ROUTER.add("/users/:username/posts/:postId", async ({ route, params }) => {
43
66
  ROUTER.start();
44
67
  ```
45
68
 
46
- ## Util functions
47
-
48
- ### `toggleParam(this, 1)`
49
-
50
- ```html
51
- <input type="checkbox" name="freelance" onclick="window.ROUTER.toggleParam(this, 1)">
52
- ```
53
-
54
- If checked, then it will append search params to url like this: `localhost/users?freelance=1`, if you click checkbox again, it will remove that search param from url.
55
-
56
69
  ## License
57
70
 
58
71
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -6,6 +6,7 @@
6
6
  * @returns {boolean}
7
7
  */
8
8
  const isFunction = (fn) => {
9
+ if (!fn) return false;
9
10
  return fn.constructor.name.toLowerCase() === "function";
10
11
  };
11
12
 
@@ -15,17 +16,19 @@ const isFunction = (fn) => {
15
16
  * @returns {boolean}
16
17
  */
17
18
  const isAsyncFunction = (fn) => {
19
+ if (!fn) return false;
18
20
  return fn.constructor.name.toLowerCase() === "asyncfunction";
19
21
  };
20
22
 
21
23
  /**
22
- * Remove trailing slash of a give url
24
+ * Add trailing slash of a given url.
23
25
  * @param {string} url
24
26
  * @returns {string}
25
27
  */
26
- const removeTrailingSlash = (url) => {
27
- if (url.endsWith("/")) {
28
- url = url.replace(/\/$/, "");
28
+ const addTrailingSlash = (url) => {
29
+ url = url.trim();
30
+ if (!url.endsWith("/")) {
31
+ url = url + "/";
29
32
  }
30
33
 
31
34
  return url;
@@ -93,210 +96,281 @@ const shouldRouterHandleClick = (e, anchor) => {
93
96
  * Class representing a router.
94
97
  */
95
98
  class BreezeRouter {
96
- /**
97
- * Creates a new BreezeRouter instance.
98
- * @constructor
99
- */
100
- constructor() {
101
- /**
102
- * Object containing all registered routes.
103
- * @type {import('./types.js').Route}
104
- * @private
105
- */
106
- this._routes = {};
107
-
108
- /**
109
- * The previous route that was navigated to
110
- * @type {import('./types.js').Route}
111
- * @private
112
- */
113
- this._previousRoute = {};
114
-
115
- // Bind event listeners
116
- window.addEventListener("popstate", this._onChanged.bind(this));
117
- document.body.addEventListener("click", this._handleClick.bind(this));
118
- }
119
-
120
- /**
121
- * Starts the router.
122
- * @returns {void}
123
- */
124
- start() {
125
- this._onChanged();
126
- }
127
-
128
- /**
129
- * Adds a new route to the router.
130
- * @param {string} route - The route path to add.
131
- * @param {Function} handler - The async function to handle the route
132
- * @returns {BreezeRouter|void} The BreezeRouter instance.
133
- */
134
- add(route, handler) {
135
- route = route.trim();
136
- if (route !== "/") {
137
- route = removeTrailingSlash(route.trim());
138
- }
139
-
140
- if (this._routes[route]) {
141
- return console.warn(`Route already exists: ${route}`);
142
- }
143
-
144
- if (typeof handler !== "function") {
145
- return console.error(`handler on route '${route}' is not a function.`);
146
- }
147
-
148
- this._routes[route] = {
149
- path: route,
150
- handler,
151
- };
152
-
153
- return this;
154
- }
155
-
156
- /**
157
- * Navigates to the specified URL.
158
- * @param {string} url - The URL to navigate to
159
- * @returns {void}
160
- */
161
- navigateTo(url) {
162
- window.history.pushState({ url }, "", url);
163
- this._onChanged();
164
- }
165
-
166
- /**
167
- * Redirects a URL
168
- * @param {string} url
169
- * @returns {void}
170
- */
171
- redirect(url) {
172
- this.navigateTo(url);
173
- }
174
-
175
- async _onChanged() {
176
- const path = window.location.pathname;
177
- const { route, params } = this._matchUrlToRoute(path);
178
-
179
- // If no matching route found, route will be '404' route
180
- // which has been handled by _matchUrlToRoute already
181
- await this._handleRoute({ route, params });
182
-
183
- /**
184
- * Update previous route, so application route handler can check and decide if it should re-render whole page.
185
- */
186
- this._previousRoute = route;
187
- }
188
-
189
- /**
190
- * Processes route callbacks registered by app
191
- * @param {import('./types.js').MatchedRoute} options
192
- * @returns {Promise<void>}
193
- */
194
- async _handleRoute({ route, params }) {
195
- if (isFunction(route.handler)) {
196
- route.handler({ route, params });
197
- }
198
-
199
- if (isAsyncFunction(route.handler)) {
200
- await route.handler({ route, params });
201
- }
202
- }
203
-
204
- /**
205
- *
206
- * @param {string} url - Current url users visite or nagivate to.
207
- * @returns {import('./types.js').MatchedRoute}
208
- */
209
- _matchUrlToRoute(url) {
210
- /** @type {import('./types.js').RouteParams} */
211
- const params = {};
212
-
213
- if (url !== "/") {
214
- url = removeTrailingSlash(url);
215
- }
216
-
217
- const matchedRoute = Object.keys(this._routes).find((route) => {
218
- if (url.split("/").length !== route.split("/").length) {
219
- return false;
220
- }
221
-
222
- let routeSegments = route.split("/").slice(1);
223
- let urlSegments = url.split("/").slice(1);
224
-
225
- // If each segment in the url matches the corresponding segment in the route path,
226
- // or the route path segment starts with a ':' then the route is matched.
227
- const match = routeSegments.every((segment, i) => {
228
- return segment === urlSegments[i] || segment.startsWith(":");
229
- });
230
-
231
- if (!match) {
232
- return false;
233
- }
234
-
235
- // If the route matches the URL, pull out any params from the URL.
236
- routeSegments.forEach((segment, i) => {
237
- if (segment.startsWith(":")) {
238
- const propName = segment.slice(1);
239
- params[propName] = decodeURIComponent(urlSegments[i]);
240
- }
241
- });
242
-
243
- return true;
244
- });
245
-
246
- if (matchedRoute) {
247
- return { route: this._routes[matchedRoute], params };
248
- } else {
249
- return { route: this._routes[404], params };
250
- }
251
- }
252
-
253
- /**
254
- * Handles <a> link clicks
255
- * @param {Event} event
256
- * @returns {void}
257
- */
258
- _handleClick(event) {
259
- const anchor = findAnchor(event);
260
- if (!anchor) {
261
- return;
262
- }
263
-
264
- if (!shouldRouterHandleClick(event, anchor)) {
265
- return;
266
- }
267
-
268
- event.preventDefault();
269
- let href = anchor.getAttribute("href")?.trim();
270
- if (!href?.startsWith("/")) {
271
- href = "/" + href;
272
- }
273
-
274
- this.navigateTo(href);
275
- }
276
-
277
- /**
278
- * Add or remove search param to current url.
279
- * @param {HTMLInputElement} checkbox
280
- * @param {string} value
281
- * @returns void
282
- */
283
- toggleParam(checkbox, value) {
284
- const params = new URLSearchParams(location.search);
285
- const name = checkbox.getAttribute('name');
286
- if (!name) {
287
- return console.warn(`name attribute is not set on ${checkbox.outerHTML}`);
288
- }
289
- if (checkbox.checked) {
290
- !params.has(name) && params.set(name, value);
291
- } else if (!checkbox.checked) {
292
- params.has(name) && params.delete(name);
293
- }
294
-
295
- const newUrl = !!params.size
296
- ? `${location.pathname}?${params.toString()}`
297
- : location.pathname;
298
- this.navigateTo(newUrl);
299
- }
99
+ /**
100
+ * `data-` attribute to allow users to skip the router behavior,
101
+ * anchor link with this attribute set will behavior as normal link.
102
+ * @type {string}
103
+ */
104
+ #ignoreAttribute = "data-breeze-ignore";
105
+
106
+ /**
107
+ * Creates a new BreezeRouter instance.
108
+ * @param {object} config - Config.
109
+ * @constructor
110
+ */
111
+ constructor(config) {
112
+ const { ignoreAttribute } = config || {};
113
+
114
+ if (ignoreAttribute) {
115
+ this.#ignoreAttribute = ignoreAttribute;
116
+ }
117
+
118
+ /**
119
+ * Object containing all registered routes.
120
+ * @type {object}
121
+ * @private
122
+ */
123
+ this._routes = {};
124
+
125
+ /**
126
+ * The previous route that was navigated to
127
+ * @type {import('./types.js').Route|null}
128
+ * @private
129
+ */
130
+ this._previousRoute = null;
131
+
132
+ // Bind event listeners
133
+ window.addEventListener("popstate", this._onChanged.bind(this));
134
+ document.body.addEventListener("click", this._handleClick.bind(this));
135
+ }
136
+
137
+ /**
138
+ * Starts the router.
139
+ * @returns {void}
140
+ */
141
+ start() {
142
+ this._onChanged();
143
+ }
144
+
145
+ /**
146
+ * Adds a new route to the router.
147
+ * @param {string} route - The route path to add.
148
+ * @param {Function} handler - The async function to handle the route
149
+ * @returns {BreezeRouter|void} The BreezeRouter instance.
150
+ */
151
+ add(route, handler) {
152
+ route = route.trim();
153
+ if (route !== "/") {
154
+ route = addTrailingSlash(route);
155
+ }
156
+
157
+ if (this._routes[route]) {
158
+ return console.warn(`Route already exists: ${route}`);
159
+ }
160
+
161
+ if (typeof handler !== "function") {
162
+ return console.error(`handler of route '${route}' must be a function.`);
163
+ }
164
+
165
+ const types = {};
166
+
167
+ // Route has types defined.
168
+ if (route.includes('<')) {
169
+ // Split the URL segments by '/'.
170
+ const urlSegments = route.split('/').filter(segment => segment.includes('<'));
171
+
172
+ // Loop through each segment and get its type.
173
+ urlSegments.forEach(segment => {
174
+ // Get the type.
175
+ const type = segment.match(/\<.+\>/)?.[0]
176
+ .replace('<', '')
177
+ .replace('>', '');
178
+
179
+ // Get the prop name.
180
+ const propName = segment.replace(/\<.+\>/, '').replace(':', '');
181
+
182
+ // Store the prop name and type.
183
+ types[propName] = type;
184
+ });
185
+ }
186
+
187
+ // Remove all '<type>' definitions.
188
+ route = route.replaceAll(/\<.+\>/g, '');
189
+
190
+ this._routes[route] = {
191
+ path: route,
192
+ types,
193
+ handler,
194
+ };
195
+
196
+ return this;
197
+ }
198
+
199
+ /**
200
+ * Navigates to the specified URL.
201
+ * @param {string} url - The URL to navigate to
202
+ * @returns {void}
203
+ */
204
+ navigateTo(url) {
205
+ window.history.pushState({ url }, "", url);
206
+ this._onChanged();
207
+ }
208
+
209
+ /**
210
+ * Redirects a URL
211
+ * @param {string} url
212
+ * @returns {void}
213
+ */
214
+ redirect(url) {
215
+ this.navigateTo(url);
216
+ }
217
+
218
+ async _onChanged() {
219
+ const path = window.location.pathname;
220
+ const { route, params } = this._matchUrlToRoute(path);
221
+
222
+ // If no matching route found, route will be '404' route
223
+ // which has been handled by _matchUrlToRoute already
224
+ await this._handleRoute({ route, params });
225
+
226
+ /**
227
+ * Update previous route, so application route handler can check and decide if it should re-render whole page.
228
+ */
229
+ this._previousRoute = route;
230
+ }
231
+
232
+ /**
233
+ * Processes route callbacks registered by app
234
+ * @param {import('./types.js').MatchedRoute} options
235
+ * @returns {Promise<void>}
236
+ */
237
+ async _handleRoute({ route, params }) {
238
+ if (isFunction(route?.handler)) {
239
+ route.handler({ route, params });
240
+ }
241
+
242
+ if (isAsyncFunction(route?.handler)) {
243
+ await route.handler({ route, params });
244
+ }
245
+ }
246
+
247
+ /**
248
+ *
249
+ * @param {string} url - Current url users visite or nagivate to.
250
+ * @returns {import('./types.js').MatchedRoute}
251
+ */
252
+ _matchUrlToRoute(url) {
253
+ /** @type {import('./types.js').RouteParams} */
254
+ const params = {};
255
+
256
+ if (url !== "/") {
257
+ url = addTrailingSlash(url);
258
+ }
259
+
260
+ const matchedRoute = Object.entries(this._routes).find(([route, routeObject]) => {
261
+ if (url.split("/").length !== route.split("/").length) {
262
+ return false;
263
+ }
264
+
265
+ const { types } = routeObject;
266
+
267
+ let routeSegments = route.split("/").slice(1);
268
+ let urlSegments = url.split("/").slice(1);
269
+
270
+ // If each segment in the url matches the corresponding segment in the route path,
271
+ // or the route path segment starts with a ':' then the route is matched.
272
+ const match = routeSegments.every((segment, i) => {
273
+ if (!segment.startsWith(":")) {
274
+ return segment === urlSegments[i];
275
+ }
276
+
277
+ // /product/:id => segment/segment
278
+ // /product/123 => urlSegment/urlSegment
279
+ const type = types[segment.replace(':', '')];
280
+
281
+ // const segmentType =
282
+ const isValid = type === 'number'
283
+ ? Number.isInteger(Number(urlSegments[i]))
284
+ : true;
285
+
286
+ return isValid;
287
+ });
288
+
289
+ if (!match) {
290
+ return false;
291
+ }
292
+
293
+ // If the route matches the URL, pull out any params from the URL.
294
+ routeSegments.forEach((segment, i) => {
295
+ if (segment.startsWith(":")) {
296
+ const propName = segment.slice(1);
297
+ params[propName] = decodeURIComponent(urlSegments[i]);
298
+ }
299
+ });
300
+
301
+ return true;
302
+ });
303
+
304
+ if (matchedRoute) {
305
+ return {
306
+ route: this._routes[matchedRoute[0]],
307
+ params,
308
+ searchParams: new URLSearchParams(window.location.search),
309
+ };
310
+ } else {
311
+ return {
312
+ route: this._routes[404],
313
+ params,
314
+ searchParams: null,
315
+ };
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Handles <a> link clicks
321
+ * @param {Event} event
322
+ * @returns {void}
323
+ */
324
+ _handleClick(event) {
325
+ const anchor = findAnchor(event);
326
+ if (!anchor) {
327
+ return;
328
+ }
329
+
330
+ /**
331
+ * Allow users to define an `data-` attribute to skip the route handling.
332
+ * Default to `data-breeze-ignore`.
333
+ */
334
+ if (anchor.hasAttribute(this.#ignoreAttribute)) {
335
+ return;
336
+ }
337
+
338
+ if (!shouldRouterHandleClick(event, anchor)) {
339
+ return;
340
+ }
341
+
342
+ event.preventDefault();
343
+ let href = anchor.getAttribute("href")?.trim();
344
+ if (!href?.startsWith("/")) {
345
+ href = "/" + href;
346
+ }
347
+
348
+ this.navigateTo(href);
349
+ }
350
+
351
+ /**
352
+ * Add or remove search param to current url.
353
+ * @param {HTMLInputElement} checkbox
354
+ * @param {string} value
355
+ * @returns void
356
+ */
357
+ toggleParam(checkbox, value) {
358
+ const params = new URLSearchParams(location.search);
359
+ const name = checkbox.getAttribute("name");
360
+ if (!name) {
361
+ return console.warn(`name attribute is not set on ${checkbox.outerHTML}`);
362
+ }
363
+ if (checkbox.checked) {
364
+ !params.has(name) && params.set(name, value);
365
+ } else if (!checkbox.checked) {
366
+ params.has(name) && params.delete(name);
367
+ }
368
+
369
+ const newUrl = !!params.size
370
+ ? `${location.pathname}?${params.toString()}`
371
+ : location.pathname;
372
+ this.navigateTo(newUrl);
373
+ }
300
374
  }
301
375
 
302
376
  export { BreezeRouter as default };
@@ -1 +1 @@
1
- {"version":3,"file":"BreezeRouter.js","sources":["../src/utils.js","../src/index.js"],"sourcesContent":["// @ts-check\n\n/**\n * Check if given param is function\n * @param {Function} fn\n * @returns {boolean}\n */\nexport const isFunction = (fn) => {\n return fn.constructor.name.toLowerCase() === \"function\";\n};\n\n/**\n * Check if given param is async function\n * @param {Function} fn\n * @returns {boolean}\n */\nexport const isAsyncFunction = (fn) => {\n return fn.constructor.name.toLowerCase() === \"asyncfunction\";\n};\n\n/**\n * Remove trailing slash of a give url\n * @param {string} url\n * @returns {string}\n */\nexport const removeTrailingSlash = (url) => {\n if (url.endsWith(\"/\")) {\n url = url.replace(/\\/$/, \"\");\n }\n\n return url;\n};\n\n/**\n * Find anchor element from click event.\n * @param {Event} e - The click event.\n * @returns {HTMLAnchorElement|undefined}\n */\nexport const findAnchor = (e) => {\n return e.composedPath().find((elem) => {\n return elem.tagName === \"A\";\n });\n};\n\n/**\n * Check if the router should handle a click event on an anchor element.\n * @param {Event} e - The click event.\n * @param {HTMLAnchorElement} anchor - The anchor element.\n * @returns {boolean} - True if the router should handle the event, false otherwise.\n */\nexport const shouldRouterHandleClick = (e, anchor) => {\n // If the event has already been handled by another event listener\n if (e.defaultPrevented) {\n return false;\n }\n\n // If the user is holding down the meta, control, or shift key\n if (e.metaKey || e.ctrlKey || e.shiftKey) {\n return false;\n }\n\n if (!anchor) {\n return false;\n }\n\n if (anchor.target) {\n return false;\n }\n\n if (\n anchor.hasAttribute(\"download\") ||\n anchor.getAttribute(\"rel\") === \"external\"\n ) {\n return false;\n }\n\n const href = anchor.href;\n if (!href || href.startsWith(\"mailto:\")) {\n return false;\n }\n\n // If the href attribute does not start with the same origin\n if (!href.startsWith(location.origin)) {\n return false;\n }\n\n return true;\n};\n","// @ts-check\nimport {\n isFunction,\n isAsyncFunction,\n removeTrailingSlash,\n findAnchor,\n shouldRouterHandleClick,\n} from \"./utils.js\";\n\n/**\n * Class representing a router.\n */\nexport default class BreezeRouter {\n /**\n * Creates a new BreezeRouter instance.\n * @constructor\n */\n constructor() {\n /**\n * Object containing all registered routes.\n * @type {import('./types.js').Route}\n * @private\n */\n this._routes = {};\n\n /**\n * The previous route that was navigated to\n * @type {import('./types.js').Route}\n * @private\n */\n this._previousRoute = {};\n\n // Bind event listeners\n window.addEventListener(\"popstate\", this._onChanged.bind(this));\n document.body.addEventListener(\"click\", this._handleClick.bind(this));\n }\n\n /**\n * Starts the router.\n * @returns {void}\n */\n start() {\n this._onChanged();\n }\n\n /**\n * Adds a new route to the router.\n * @param {string} route - The route path to add.\n * @param {Function} handler - The async function to handle the route\n * @returns {BreezeRouter|void} The BreezeRouter instance.\n */\n add(route, handler) {\n route = route.trim();\n if (route !== \"/\") {\n route = removeTrailingSlash(route.trim());\n }\n\n if (this._routes[route]) {\n return console.warn(`Route already exists: ${route}`);\n }\n\n if (typeof handler !== \"function\") {\n return console.error(`handler on route '${route}' is not a function.`);\n }\n\n this._routes[route] = {\n path: route,\n handler,\n };\n\n return this;\n }\n\n /**\n * Navigates to the specified URL.\n * @param {string} url - The URL to navigate to\n * @returns {void}\n */\n navigateTo(url) {\n window.history.pushState({ url }, \"\", url);\n this._onChanged();\n }\n\n /**\n * Redirects a URL\n * @param {string} url\n * @returns {void}\n */\n redirect(url) {\n this.navigateTo(url);\n }\n\n async _onChanged() {\n const path = window.location.pathname;\n const { route, params } = this._matchUrlToRoute(path);\n\n // If no matching route found, route will be '404' route\n // which has been handled by _matchUrlToRoute already\n await this._handleRoute({ route, params });\n\n /**\n * Update previous route, so application route handler can check and decide if it should re-render whole page.\n */\n this._previousRoute = route;\n }\n\n /**\n * Processes route callbacks registered by app\n * @param {import('./types.js').MatchedRoute} options\n * @returns {Promise<void>}\n */\n async _handleRoute({ route, params }) {\n if (isFunction(route.handler)) {\n route.handler({ route, params });\n }\n\n if (isAsyncFunction(route.handler)) {\n await route.handler({ route, params });\n }\n }\n\n /**\n *\n * @param {string} url - Current url users visite or nagivate to.\n * @returns {import('./types.js').MatchedRoute}\n */\n _matchUrlToRoute(url) {\n /** @type {import('./types.js').RouteParams} */\n const params = {};\n\n if (url !== \"/\") {\n url = removeTrailingSlash(url);\n }\n\n const matchedRoute = Object.keys(this._routes).find((route) => {\n if (url.split(\"/\").length !== route.split(\"/\").length) {\n return false;\n }\n\n let routeSegments = route.split(\"/\").slice(1);\n let urlSegments = url.split(\"/\").slice(1);\n\n // If each segment in the url matches the corresponding segment in the route path,\n // or the route path segment starts with a ':' then the route is matched.\n const match = routeSegments.every((segment, i) => {\n return segment === urlSegments[i] || segment.startsWith(\":\");\n });\n\n if (!match) {\n return false;\n }\n\n // If the route matches the URL, pull out any params from the URL.\n routeSegments.forEach((segment, i) => {\n if (segment.startsWith(\":\")) {\n const propName = segment.slice(1);\n params[propName] = decodeURIComponent(urlSegments[i]);\n }\n });\n\n return true;\n });\n\n if (matchedRoute) {\n return { route: this._routes[matchedRoute], params };\n } else {\n return { route: this._routes[404], params };\n }\n }\n\n /**\n * Handles <a> link clicks\n * @param {Event} event\n * @returns {void}\n */\n _handleClick(event) {\n const anchor = findAnchor(event);\n if (!anchor) {\n return;\n }\n\n if (!shouldRouterHandleClick(event, anchor)) {\n return;\n }\n\n event.preventDefault();\n let href = anchor.getAttribute(\"href\")?.trim();\n if (!href?.startsWith(\"/\")) {\n href = \"/\" + href;\n }\n\n this.navigateTo(href);\n }\n\n /**\n * Add or remove search param to current url.\n * @param {HTMLInputElement} checkbox \n * @param {string} value\n * @returns void\n */\n toggleParam(checkbox, value) {\n const params = new URLSearchParams(location.search);\n const name = checkbox.getAttribute('name')\n if (!name) {\n return console.warn(`name attribute is not set on ${checkbox.outerHTML}`);\n }\n if (checkbox.checked) {\n !params.has(name) && params.set(name, value);\n } else if (!checkbox.checked) {\n params.has(name) && params.delete(name);\n }\n \n const newUrl = !!params.size\n ? `${location.pathname}?${params.toString()}`\n : location.pathname;\n this.navigateTo(newUrl);\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BreezeRouter.js","sources":["../src/utils.js","../src/index.js"],"sourcesContent":["// @ts-check\n\n/**\n * Check if given param is function\n * @param {Function} fn\n * @returns {boolean}\n */\nexport const isFunction = (fn) => {\n if (!fn) return false;\n return fn.constructor.name.toLowerCase() === \"function\";\n};\n\n/**\n * Check if given param is async function\n * @param {Function} fn\n * @returns {boolean}\n */\nexport const isAsyncFunction = (fn) => {\n if (!fn) return false;\n return fn.constructor.name.toLowerCase() === \"asyncfunction\";\n};\n\n/**\n * Remove trailing slash of a given url.\n * @param {string} url\n * @returns {string}\n */\nexport const removeTrailingSlash = (url) => {\n if (url.endsWith(\"/\")) {\n url = url.replace(/\\/$/, \"\");\n }\n\n return url;\n};\n\n/**\n * Add trailing slash of a given url.\n * @param {string} url\n * @returns {string}\n */\nexport const addTrailingSlash = (url) => {\n url = url.trim();\n if (!url.endsWith(\"/\")) {\n url = url + \"/\";\n }\n\n return url;\n};\n\n/**\n * Find anchor element from click event.\n * @param {Event} e - The click event.\n * @returns {HTMLAnchorElement|undefined}\n */\nexport const findAnchor = (e) => {\n return e.composedPath().find((elem) => {\n return elem.tagName === \"A\";\n });\n};\n\n/**\n * Check if the router should handle a click event on an anchor element.\n * @param {Event} e - The click event.\n * @param {HTMLAnchorElement} anchor - The anchor element.\n * @returns {boolean} - True if the router should handle the event, false otherwise.\n */\nexport const shouldRouterHandleClick = (e, anchor) => {\n // If the event has already been handled by another event listener\n if (e.defaultPrevented) {\n return false;\n }\n\n // If the user is holding down the meta, control, or shift key\n if (e.metaKey || e.ctrlKey || e.shiftKey) {\n return false;\n }\n\n if (!anchor) {\n return false;\n }\n\n if (anchor.target) {\n return false;\n }\n\n if (\n anchor.hasAttribute(\"download\") ||\n anchor.getAttribute(\"rel\") === \"external\"\n ) {\n return false;\n }\n\n const href = anchor.href;\n if (!href || href.startsWith(\"mailto:\")) {\n return false;\n }\n\n // If the href attribute does not start with the same origin\n if (!href.startsWith(location.origin)) {\n return false;\n }\n\n return true;\n};\n","// @ts-check\nimport {\n\tisFunction,\n\tisAsyncFunction,\n\tremoveTrailingSlash,\n\taddTrailingSlash,\n\tfindAnchor,\n\tshouldRouterHandleClick,\n} from \"./utils.js\";\n\n/**\n * Class representing a router.\n */\nexport default class BreezeRouter {\n\t/**\n\t * `data-` attribute to allow users to skip the router behavior,\n\t * anchor link with this attribute set will behavior as normal link.\n\t * @type {string}\n\t */\n\t#ignoreAttribute = \"data-breeze-ignore\";\n\n\t/**\n\t * Creates a new BreezeRouter instance.\n\t * @param {object} config - Config.\n\t * @constructor\n\t */\n\tconstructor(config) {\n\t\tconst { ignoreAttribute } = config || {};\n\n\t\tif (ignoreAttribute) {\n\t\t\tthis.#ignoreAttribute = ignoreAttribute;\n\t\t}\n\n\t\t/**\n\t\t * Object containing all registered routes.\n\t\t * @type {object}\n\t\t * @private\n\t\t */\n\t\tthis._routes = {};\n\n\t\t/**\n\t\t * The previous route that was navigated to\n\t\t * @type {import('./types.js').Route|null}\n\t\t * @private\n\t\t */\n\t\tthis._previousRoute = null;\n\n\t\t// Bind event listeners\n\t\twindow.addEventListener(\"popstate\", this._onChanged.bind(this));\n\t\tdocument.body.addEventListener(\"click\", this._handleClick.bind(this));\n\t}\n\n\t/**\n\t * Starts the router.\n\t * @returns {void}\n\t */\n\tstart() {\n\t\tthis._onChanged();\n\t}\n\n\t/**\n\t * Adds a new route to the router.\n\t * @param {string} route - The route path to add.\n\t * @param {Function} handler - The async function to handle the route\n\t * @returns {BreezeRouter|void} The BreezeRouter instance.\n\t */\n\tadd(route, handler) {\n\t\troute = route.trim();\n\t\tif (route !== \"/\") {\n\t\t\troute = addTrailingSlash(route);\n\t\t}\n\n\t\tif (this._routes[route]) {\n\t\t\treturn console.warn(`Route already exists: ${route}`);\n\t\t}\n\n\t\tif (typeof handler !== \"function\") {\n\t\t\treturn console.error(`handler of route '${route}' must be a function.`);\n\t\t}\n\n\t\tconst types = {};\n\n\t\t// Route has types defined.\n\t\tif (route.includes('<')) {\n\t\t\t// Split the URL segments by '/'.\n\t\t\tconst urlSegments = route.split('/').filter(segment => segment.includes('<'));\n\n\t\t\t// Loop through each segment and get its type.\n\t\t\turlSegments.forEach(segment => {\n\t\t\t\t// Get the type.\n\t\t\t\tconst type = segment.match(/\\<.+\\>/)?.[0]\n\t\t\t\t\t.replace('<', '')\n\t\t\t\t\t.replace('>', '');\n\n\t\t\t\t// Get the prop name.\n\t\t\t\tconst propName = segment.replace(/\\<.+\\>/, '').replace(':', '');\n\n\t\t\t\t// Store the prop name and type.\n\t\t\t\ttypes[propName] = type;\n\t\t\t});\n\t\t}\n\n\t\t// Remove all '<type>' definitions.\n\t\troute = route.replaceAll(/\\<.+\\>/g, '');\n\n\t\tthis._routes[route] = {\n\t\t\tpath: route,\n\t\t\ttypes,\n\t\t\thandler,\n\t\t};\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Navigates to the specified URL.\n\t * @param {string} url - The URL to navigate to\n\t * @returns {void}\n\t */\n\tnavigateTo(url) {\n\t\twindow.history.pushState({ url }, \"\", url);\n\t\tthis._onChanged();\n\t}\n\n\t/**\n\t * Redirects a URL\n\t * @param {string} url\n\t * @returns {void}\n\t */\n\tredirect(url) {\n\t\tthis.navigateTo(url);\n\t}\n\n\tasync _onChanged() {\n\t\tconst path = window.location.pathname;\n\t\tconst { route, params } = this._matchUrlToRoute(path);\n\n\t\t// If no matching route found, route will be '404' route\n\t\t// which has been handled by _matchUrlToRoute already\n\t\tawait this._handleRoute({ route, params });\n\n\t\t/**\n\t\t * Update previous route, so application route handler can check and decide if it should re-render whole page.\n\t\t */\n\t\tthis._previousRoute = route;\n\t}\n\n\t/**\n\t * Processes route callbacks registered by app\n\t * @param {import('./types.js').MatchedRoute} options\n\t * @returns {Promise<void>}\n\t */\n\tasync _handleRoute({ route, params }) {\n\t\tif (isFunction(route?.handler)) {\n\t\t\troute.handler({ route, params });\n\t\t}\n\n\t\tif (isAsyncFunction(route?.handler)) {\n\t\t\tawait route.handler({ route, params });\n\t\t}\n\t}\n\n\t/**\n\t *\n\t * @param {string} url - Current url users visite or nagivate to.\n\t * @returns {import('./types.js').MatchedRoute}\n\t */\n\t_matchUrlToRoute(url) {\n\t\t/** @type {import('./types.js').RouteParams} */\n\t\tconst params = {};\n\n\t\tif (url !== \"/\") {\n\t\t\turl = addTrailingSlash(url);\n\t\t}\n\n\t\tconst matchedRoute = Object.entries(this._routes).find(([route, routeObject]) => {\n\t\t\tif (url.split(\"/\").length !== route.split(\"/\").length) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst { types } = routeObject;\n\n\t\t\tlet routeSegments = route.split(\"/\").slice(1);\n\t\t\tlet urlSegments = url.split(\"/\").slice(1);\n\n\t\t\t// If each segment in the url matches the corresponding segment in the route path,\n\t\t\t// or the route path segment starts with a ':' then the route is matched.\n\t\t\tconst match = routeSegments.every((segment, i) => {\n\t\t\t\tif (!segment.startsWith(\":\")) {\n\t\t\t\t\treturn segment === urlSegments[i];\n\t\t\t\t}\n\n\t\t\t\t// /product/:id => segment/segment\n\t\t\t\t// /product/123 => urlSegment/urlSegment\n\t\t\t\tconst type = types[segment.replace(':', '')];\n\n\t\t\t\t// const segmentType = \n\t\t\t\tconst isValid = type === 'number'\n\t\t\t\t\t? Number.isInteger(Number(urlSegments[i]))\n\t\t\t\t\t: true;\n\n\t\t\t\treturn isValid;\n\t\t\t});\n\n\t\t\tif (!match) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// If the route matches the URL, pull out any params from the URL.\n\t\t\trouteSegments.forEach((segment, i) => {\n\t\t\t\tif (segment.startsWith(\":\")) {\n\t\t\t\t\tconst propName = segment.slice(1);\n\t\t\t\t\tparams[propName] = decodeURIComponent(urlSegments[i]);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\treturn true;\n\t\t});\n\n\t\tif (matchedRoute) {\n\t\t\treturn {\n\t\t\t\troute: this._routes[matchedRoute[0]],\n\t\t\t\tparams,\n\t\t\t\tsearchParams: new URLSearchParams(window.location.search),\n\t\t\t};\n\t\t} else {\n\t\t\treturn {\n\t\t\t\troute: this._routes[404],\n\t\t\t\tparams,\n\t\t\t\tsearchParams: null,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Handles <a> link clicks\n\t * @param {Event} event\n\t * @returns {void}\n\t */\n\t_handleClick(event) {\n\t\tconst anchor = findAnchor(event);\n\t\tif (!anchor) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Allow users to define an `data-` attribute to skip the route handling.\n\t\t * Default to `data-breeze-ignore`.\n\t\t */\n\t\tif (anchor.hasAttribute(this.#ignoreAttribute)) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!shouldRouterHandleClick(event, anchor)) {\n\t\t\treturn;\n\t\t}\n\n\t\tevent.preventDefault();\n\t\tlet href = anchor.getAttribute(\"href\")?.trim();\n\t\tif (!href?.startsWith(\"/\")) {\n\t\t\thref = \"/\" + href;\n\t\t}\n\n\t\tthis.navigateTo(href);\n\t}\n\n\t/**\n\t * Add or remove search param to current url.\n\t * @param {HTMLInputElement} checkbox\n\t * @param {string} value\n\t * @returns void\n\t */\n\ttoggleParam(checkbox, value) {\n\t\tconst params = new URLSearchParams(location.search);\n\t\tconst name = checkbox.getAttribute(\"name\");\n\t\tif (!name) {\n\t\t\treturn console.warn(`name attribute is not set on ${checkbox.outerHTML}`);\n\t\t}\n\t\tif (checkbox.checked) {\n\t\t\t!params.has(name) && params.set(name, value);\n\t\t} else if (!checkbox.checked) {\n\t\t\tparams.has(name) && params.delete(name);\n\t\t}\n\n\t\tconst newUrl = !!params.size\n\t\t\t? `${location.pathname}?${params.toString()}`\n\t\t\t: location.pathname;\n\t\tthis.navigateTo(newUrl);\n\t}\n}\n"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const t=t=>(t.endsWith("/")&&(t=t.replace(/\/$/,"")),t);class e{constructor(){this._routes={},this._previousRoute={},window.addEventListener("popstate",this._onChanged.bind(this)),document.body.addEventListener("click",this._handleClick.bind(this))}start(){this._onChanged()}add(e,r){return"/"!==(e=e.trim())&&(e=t(e.trim())),this._routes[e]?console.warn(`Route already exists: ${e}`):"function"!=typeof r?console.error(`handler on route '${e}' is not a function.`):(this._routes[e]={path:e,handler:r},this)}navigateTo(t){window.history.pushState({url:t},"",t),this._onChanged()}redirect(t){this.navigateTo(t)}async _onChanged(){const t=window.location.pathname,{route:e,params:r}=this._matchUrlToRoute(t);await this._handleRoute({route:e,params:r}),this._previousRoute=e}async _handleRoute({route:t,params:e}){"function"===t.handler.constructor.name.toLowerCase()&&t.handler({route:t,params:e}),(t=>"asyncfunction"===t.constructor.name.toLowerCase())(t.handler)&&await t.handler({route:t,params:e})}_matchUrlToRoute(e){const r={};"/"!==e&&(e=t(e));const a=Object.keys(this._routes).find((t=>{if(e.split("/").length!==t.split("/").length)return!1;let a=t.split("/").slice(1),n=e.split("/").slice(1);return!!a.every(((t,e)=>t===n[e]||t.startsWith(":")))&&(a.forEach(((t,e)=>{if(t.startsWith(":")){const a=t.slice(1);r[a]=decodeURIComponent(n[e])}})),!0)}));return a?{route:this._routes[a],params:r}:{route:this._routes[404],params:r}}_handleClick(t){const e=t.composedPath().find((t=>"A"===t.tagName));if(!e)return;if(!((t,e)=>{if(t.defaultPrevented)return!1;if(t.metaKey||t.ctrlKey||t.shiftKey)return!1;if(!e)return!1;if(e.target)return!1;if(e.hasAttribute("download")||"external"===e.getAttribute("rel"))return!1;const r=e.href;return!(!r||r.startsWith("mailto:")||!r.startsWith(location.origin))})(t,e))return;t.preventDefault();let r=e.getAttribute("href")?.trim();r?.startsWith("/")||(r="/"+r),this.navigateTo(r)}toggleParam(t,e){const r=new URLSearchParams(location.search),a=t.getAttribute("name");if(!a)return console.warn(`name attribute is not set on ${t.outerHTML}`);t.checked?!r.has(a)&&r.set(a,e):t.checked||r.has(a)&&r.delete(a);const n=r.size?`${location.pathname}?${r.toString()}`:location.pathname;this.navigateTo(n)}}export{e as default};
1
+ const t=t=>((t=t.trim()).endsWith("/")||(t+="/"),t);class e{#t="data-breeze-ignore";constructor(t){const{ignoreAttribute:e}=t||{};e&&(this.#t=e),this._routes={},this._previousRoute=null,window.addEventListener("popstate",this._onChanged.bind(this)),document.body.addEventListener("click",this._handleClick.bind(this))}start(){this._onChanged()}add(e,r){if("/"!==(e=e.trim())&&(e=t(e)),this._routes[e])return console.warn(`Route already exists: ${e}`);if("function"!=typeof r)return console.error(`handler of route '${e}' must be a function.`);const a={};if(e.includes("<")){e.split("/").filter((t=>t.includes("<"))).forEach((t=>{const e=t.match(/\<.+\>/)?.[0].replace("<","").replace(">",""),r=t.replace(/\<.+\>/,"").replace(":","");a[r]=e}))}return e=e.replaceAll(/\<.+\>/g,""),this._routes[e]={path:e,types:a,handler:r},this}navigateTo(t){window.history.pushState({url:t},"",t),this._onChanged()}redirect(t){this.navigateTo(t)}async _onChanged(){const t=window.location.pathname,{route:e,params:r}=this._matchUrlToRoute(t);await this._handleRoute({route:e,params:r}),this._previousRoute=e}async _handleRoute({route:t,params:e}){var r;r=t?.handler,r&&"function"===r.constructor.name.toLowerCase()&&t.handler({route:t,params:e}),(t=>!!t&&"asyncfunction"===t.constructor.name.toLowerCase())(t?.handler)&&await t.handler({route:t,params:e})}_matchUrlToRoute(e){const r={};"/"!==e&&(e=t(e));const a=Object.entries(this._routes).find((([t,a])=>{if(e.split("/").length!==t.split("/").length)return!1;const{types:n}=a;let s=t.split("/").slice(1),i=e.split("/").slice(1);return!!s.every(((t,e)=>{if(!t.startsWith(":"))return t===i[e];return"number"!==n[t.replace(":","")]||Number.isInteger(Number(i[e]))}))&&(s.forEach(((t,e)=>{if(t.startsWith(":")){const a=t.slice(1);r[a]=decodeURIComponent(i[e])}})),!0)}));return a?{route:this._routes[a[0]],params:r,searchParams:new URLSearchParams(window.location.search)}:{route:this._routes[404],params:r,searchParams:null}}_handleClick(t){const e=t.composedPath().find((t=>"A"===t.tagName));if(!e)return;if(e.hasAttribute(this.#t))return;if(!((t,e)=>{if(t.defaultPrevented)return!1;if(t.metaKey||t.ctrlKey||t.shiftKey)return!1;if(!e)return!1;if(e.target)return!1;if(e.hasAttribute("download")||"external"===e.getAttribute("rel"))return!1;const r=e.href;return!(!r||r.startsWith("mailto:")||!r.startsWith(location.origin))})(t,e))return;t.preventDefault();let r=e.getAttribute("href")?.trim();r?.startsWith("/")||(r="/"+r),this.navigateTo(r)}toggleParam(t,e){const r=new URLSearchParams(location.search),a=t.getAttribute("name");if(!a)return console.warn(`name attribute is not set on ${t.outerHTML}`);t.checked?!r.has(a)&&r.set(a,e):t.checked||r.has(a)&&r.delete(a);const n=r.size?`${location.pathname}?${r.toString()}`:location.pathname;this.navigateTo(n)}}export{e as default};
2
2
  //# sourceMappingURL=BreezeRouter.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"BreezeRouter.min.js","sources":["../src/utils.js","../src/index.js"],"sourcesContent":["// @ts-check\n\n/**\n * Check if given param is function\n * @param {Function} fn\n * @returns {boolean}\n */\nexport const isFunction = (fn) => {\n return fn.constructor.name.toLowerCase() === \"function\";\n};\n\n/**\n * Check if given param is async function\n * @param {Function} fn\n * @returns {boolean}\n */\nexport const isAsyncFunction = (fn) => {\n return fn.constructor.name.toLowerCase() === \"asyncfunction\";\n};\n\n/**\n * Remove trailing slash of a give url\n * @param {string} url\n * @returns {string}\n */\nexport const removeTrailingSlash = (url) => {\n if (url.endsWith(\"/\")) {\n url = url.replace(/\\/$/, \"\");\n }\n\n return url;\n};\n\n/**\n * Find anchor element from click event.\n * @param {Event} e - The click event.\n * @returns {HTMLAnchorElement|undefined}\n */\nexport const findAnchor = (e) => {\n return e.composedPath().find((elem) => {\n return elem.tagName === \"A\";\n });\n};\n\n/**\n * Check if the router should handle a click event on an anchor element.\n * @param {Event} e - The click event.\n * @param {HTMLAnchorElement} anchor - The anchor element.\n * @returns {boolean} - True if the router should handle the event, false otherwise.\n */\nexport const shouldRouterHandleClick = (e, anchor) => {\n // If the event has already been handled by another event listener\n if (e.defaultPrevented) {\n return false;\n }\n\n // If the user is holding down the meta, control, or shift key\n if (e.metaKey || e.ctrlKey || e.shiftKey) {\n return false;\n }\n\n if (!anchor) {\n return false;\n }\n\n if (anchor.target) {\n return false;\n }\n\n if (\n anchor.hasAttribute(\"download\") ||\n anchor.getAttribute(\"rel\") === \"external\"\n ) {\n return false;\n }\n\n const href = anchor.href;\n if (!href || href.startsWith(\"mailto:\")) {\n return false;\n }\n\n // If the href attribute does not start with the same origin\n if (!href.startsWith(location.origin)) {\n return false;\n }\n\n return true;\n};\n","// @ts-check\nimport {\n isFunction,\n isAsyncFunction,\n removeTrailingSlash,\n findAnchor,\n shouldRouterHandleClick,\n} from \"./utils.js\";\n\n/**\n * Class representing a router.\n */\nexport default class BreezeRouter {\n /**\n * Creates a new BreezeRouter instance.\n * @constructor\n */\n constructor() {\n /**\n * Object containing all registered routes.\n * @type {import('./types.js').Route}\n * @private\n */\n this._routes = {};\n\n /**\n * The previous route that was navigated to\n * @type {import('./types.js').Route}\n * @private\n */\n this._previousRoute = {};\n\n // Bind event listeners\n window.addEventListener(\"popstate\", this._onChanged.bind(this));\n document.body.addEventListener(\"click\", this._handleClick.bind(this));\n }\n\n /**\n * Starts the router.\n * @returns {void}\n */\n start() {\n this._onChanged();\n }\n\n /**\n * Adds a new route to the router.\n * @param {string} route - The route path to add.\n * @param {Function} handler - The async function to handle the route\n * @returns {BreezeRouter|void} The BreezeRouter instance.\n */\n add(route, handler) {\n route = route.trim();\n if (route !== \"/\") {\n route = removeTrailingSlash(route.trim());\n }\n\n if (this._routes[route]) {\n return console.warn(`Route already exists: ${route}`);\n }\n\n if (typeof handler !== \"function\") {\n return console.error(`handler on route '${route}' is not a function.`);\n }\n\n this._routes[route] = {\n path: route,\n handler,\n };\n\n return this;\n }\n\n /**\n * Navigates to the specified URL.\n * @param {string} url - The URL to navigate to\n * @returns {void}\n */\n navigateTo(url) {\n window.history.pushState({ url }, \"\", url);\n this._onChanged();\n }\n\n /**\n * Redirects a URL\n * @param {string} url\n * @returns {void}\n */\n redirect(url) {\n this.navigateTo(url);\n }\n\n async _onChanged() {\n const path = window.location.pathname;\n const { route, params } = this._matchUrlToRoute(path);\n\n // If no matching route found, route will be '404' route\n // which has been handled by _matchUrlToRoute already\n await this._handleRoute({ route, params });\n\n /**\n * Update previous route, so application route handler can check and decide if it should re-render whole page.\n */\n this._previousRoute = route;\n }\n\n /**\n * Processes route callbacks registered by app\n * @param {import('./types.js').MatchedRoute} options\n * @returns {Promise<void>}\n */\n async _handleRoute({ route, params }) {\n if (isFunction(route.handler)) {\n route.handler({ route, params });\n }\n\n if (isAsyncFunction(route.handler)) {\n await route.handler({ route, params });\n }\n }\n\n /**\n *\n * @param {string} url - Current url users visite or nagivate to.\n * @returns {import('./types.js').MatchedRoute}\n */\n _matchUrlToRoute(url) {\n /** @type {import('./types.js').RouteParams} */\n const params = {};\n\n if (url !== \"/\") {\n url = removeTrailingSlash(url);\n }\n\n const matchedRoute = Object.keys(this._routes).find((route) => {\n if (url.split(\"/\").length !== route.split(\"/\").length) {\n return false;\n }\n\n let routeSegments = route.split(\"/\").slice(1);\n let urlSegments = url.split(\"/\").slice(1);\n\n // If each segment in the url matches the corresponding segment in the route path,\n // or the route path segment starts with a ':' then the route is matched.\n const match = routeSegments.every((segment, i) => {\n return segment === urlSegments[i] || segment.startsWith(\":\");\n });\n\n if (!match) {\n return false;\n }\n\n // If the route matches the URL, pull out any params from the URL.\n routeSegments.forEach((segment, i) => {\n if (segment.startsWith(\":\")) {\n const propName = segment.slice(1);\n params[propName] = decodeURIComponent(urlSegments[i]);\n }\n });\n\n return true;\n });\n\n if (matchedRoute) {\n return { route: this._routes[matchedRoute], params };\n } else {\n return { route: this._routes[404], params };\n }\n }\n\n /**\n * Handles <a> link clicks\n * @param {Event} event\n * @returns {void}\n */\n _handleClick(event) {\n const anchor = findAnchor(event);\n if (!anchor) {\n return;\n }\n\n if (!shouldRouterHandleClick(event, anchor)) {\n return;\n }\n\n event.preventDefault();\n let href = anchor.getAttribute(\"href\")?.trim();\n if (!href?.startsWith(\"/\")) {\n href = \"/\" + href;\n }\n\n this.navigateTo(href);\n }\n\n /**\n * Add or remove search param to current url.\n * @param {HTMLInputElement} checkbox \n * @param {string} value\n * @returns void\n */\n toggleParam(checkbox, value) {\n const params = new URLSearchParams(location.search);\n const name = checkbox.getAttribute('name')\n if (!name) {\n return console.warn(`name attribute is not set on ${checkbox.outerHTML}`);\n }\n if (checkbox.checked) {\n !params.has(name) && params.set(name, value);\n } else if (!checkbox.checked) {\n params.has(name) && params.delete(name);\n }\n \n const newUrl = !!params.size\n ? `${location.pathname}?${params.toString()}`\n : location.pathname;\n this.navigateTo(newUrl);\n }\n}\n"],"names":["removeTrailingSlash","url","endsWith","replace","BreezeRouter","constructor","this","_routes","_previousRoute","window","addEventListener","_onChanged","bind","document","body","_handleClick","start","add","route","handler","trim","console","warn","error","path","navigateTo","history","pushState","redirect","async","location","pathname","params","_matchUrlToRoute","_handleRoute","name","toLowerCase","fn","isAsyncFunction","matchedRoute","Object","keys","find","split","length","routeSegments","slice","urlSegments","every","segment","i","startsWith","forEach","propName","decodeURIComponent","event","anchor","composedPath","elem","tagName","e","defaultPrevented","metaKey","ctrlKey","shiftKey","target","hasAttribute","getAttribute","href","origin","shouldRouterHandleClick","preventDefault","toggleParam","checkbox","value","URLSearchParams","search","outerHTML","checked","has","set","delete","newUrl","size","toString"],"mappings":"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"}
1
+ {"version":3,"file":"BreezeRouter.min.js","sources":["../src/utils.js","../src/index.js"],"sourcesContent":["// @ts-check\n\n/**\n * Check if given param is function\n * @param {Function} fn\n * @returns {boolean}\n */\nexport const isFunction = (fn) => {\n if (!fn) return false;\n return fn.constructor.name.toLowerCase() === \"function\";\n};\n\n/**\n * Check if given param is async function\n * @param {Function} fn\n * @returns {boolean}\n */\nexport const isAsyncFunction = (fn) => {\n if (!fn) return false;\n return fn.constructor.name.toLowerCase() === \"asyncfunction\";\n};\n\n/**\n * Remove trailing slash of a given url.\n * @param {string} url\n * @returns {string}\n */\nexport const removeTrailingSlash = (url) => {\n if (url.endsWith(\"/\")) {\n url = url.replace(/\\/$/, \"\");\n }\n\n return url;\n};\n\n/**\n * Add trailing slash of a given url.\n * @param {string} url\n * @returns {string}\n */\nexport const addTrailingSlash = (url) => {\n url = url.trim();\n if (!url.endsWith(\"/\")) {\n url = url + \"/\";\n }\n\n return url;\n};\n\n/**\n * Find anchor element from click event.\n * @param {Event} e - The click event.\n * @returns {HTMLAnchorElement|undefined}\n */\nexport const findAnchor = (e) => {\n return e.composedPath().find((elem) => {\n return elem.tagName === \"A\";\n });\n};\n\n/**\n * Check if the router should handle a click event on an anchor element.\n * @param {Event} e - The click event.\n * @param {HTMLAnchorElement} anchor - The anchor element.\n * @returns {boolean} - True if the router should handle the event, false otherwise.\n */\nexport const shouldRouterHandleClick = (e, anchor) => {\n // If the event has already been handled by another event listener\n if (e.defaultPrevented) {\n return false;\n }\n\n // If the user is holding down the meta, control, or shift key\n if (e.metaKey || e.ctrlKey || e.shiftKey) {\n return false;\n }\n\n if (!anchor) {\n return false;\n }\n\n if (anchor.target) {\n return false;\n }\n\n if (\n anchor.hasAttribute(\"download\") ||\n anchor.getAttribute(\"rel\") === \"external\"\n ) {\n return false;\n }\n\n const href = anchor.href;\n if (!href || href.startsWith(\"mailto:\")) {\n return false;\n }\n\n // If the href attribute does not start with the same origin\n if (!href.startsWith(location.origin)) {\n return false;\n }\n\n return true;\n};\n","// @ts-check\nimport {\n\tisFunction,\n\tisAsyncFunction,\n\tremoveTrailingSlash,\n\taddTrailingSlash,\n\tfindAnchor,\n\tshouldRouterHandleClick,\n} from \"./utils.js\";\n\n/**\n * Class representing a router.\n */\nexport default class BreezeRouter {\n\t/**\n\t * `data-` attribute to allow users to skip the router behavior,\n\t * anchor link with this attribute set will behavior as normal link.\n\t * @type {string}\n\t */\n\t#ignoreAttribute = \"data-breeze-ignore\";\n\n\t/**\n\t * Creates a new BreezeRouter instance.\n\t * @param {object} config - Config.\n\t * @constructor\n\t */\n\tconstructor(config) {\n\t\tconst { ignoreAttribute } = config || {};\n\n\t\tif (ignoreAttribute) {\n\t\t\tthis.#ignoreAttribute = ignoreAttribute;\n\t\t}\n\n\t\t/**\n\t\t * Object containing all registered routes.\n\t\t * @type {object}\n\t\t * @private\n\t\t */\n\t\tthis._routes = {};\n\n\t\t/**\n\t\t * The previous route that was navigated to\n\t\t * @type {import('./types.js').Route|null}\n\t\t * @private\n\t\t */\n\t\tthis._previousRoute = null;\n\n\t\t// Bind event listeners\n\t\twindow.addEventListener(\"popstate\", this._onChanged.bind(this));\n\t\tdocument.body.addEventListener(\"click\", this._handleClick.bind(this));\n\t}\n\n\t/**\n\t * Starts the router.\n\t * @returns {void}\n\t */\n\tstart() {\n\t\tthis._onChanged();\n\t}\n\n\t/**\n\t * Adds a new route to the router.\n\t * @param {string} route - The route path to add.\n\t * @param {Function} handler - The async function to handle the route\n\t * @returns {BreezeRouter|void} The BreezeRouter instance.\n\t */\n\tadd(route, handler) {\n\t\troute = route.trim();\n\t\tif (route !== \"/\") {\n\t\t\troute = addTrailingSlash(route);\n\t\t}\n\n\t\tif (this._routes[route]) {\n\t\t\treturn console.warn(`Route already exists: ${route}`);\n\t\t}\n\n\t\tif (typeof handler !== \"function\") {\n\t\t\treturn console.error(`handler of route '${route}' must be a function.`);\n\t\t}\n\n\t\tconst types = {};\n\n\t\t// Route has types defined.\n\t\tif (route.includes('<')) {\n\t\t\t// Split the URL segments by '/'.\n\t\t\tconst urlSegments = route.split('/').filter(segment => segment.includes('<'));\n\n\t\t\t// Loop through each segment and get its type.\n\t\t\turlSegments.forEach(segment => {\n\t\t\t\t// Get the type.\n\t\t\t\tconst type = segment.match(/\\<.+\\>/)?.[0]\n\t\t\t\t\t.replace('<', '')\n\t\t\t\t\t.replace('>', '');\n\n\t\t\t\t// Get the prop name.\n\t\t\t\tconst propName = segment.replace(/\\<.+\\>/, '').replace(':', '');\n\n\t\t\t\t// Store the prop name and type.\n\t\t\t\ttypes[propName] = type;\n\t\t\t});\n\t\t}\n\n\t\t// Remove all '<type>' definitions.\n\t\troute = route.replaceAll(/\\<.+\\>/g, '');\n\n\t\tthis._routes[route] = {\n\t\t\tpath: route,\n\t\t\ttypes,\n\t\t\thandler,\n\t\t};\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Navigates to the specified URL.\n\t * @param {string} url - The URL to navigate to\n\t * @returns {void}\n\t */\n\tnavigateTo(url) {\n\t\twindow.history.pushState({ url }, \"\", url);\n\t\tthis._onChanged();\n\t}\n\n\t/**\n\t * Redirects a URL\n\t * @param {string} url\n\t * @returns {void}\n\t */\n\tredirect(url) {\n\t\tthis.navigateTo(url);\n\t}\n\n\tasync _onChanged() {\n\t\tconst path = window.location.pathname;\n\t\tconst { route, params } = this._matchUrlToRoute(path);\n\n\t\t// If no matching route found, route will be '404' route\n\t\t// which has been handled by _matchUrlToRoute already\n\t\tawait this._handleRoute({ route, params });\n\n\t\t/**\n\t\t * Update previous route, so application route handler can check and decide if it should re-render whole page.\n\t\t */\n\t\tthis._previousRoute = route;\n\t}\n\n\t/**\n\t * Processes route callbacks registered by app\n\t * @param {import('./types.js').MatchedRoute} options\n\t * @returns {Promise<void>}\n\t */\n\tasync _handleRoute({ route, params }) {\n\t\tif (isFunction(route?.handler)) {\n\t\t\troute.handler({ route, params });\n\t\t}\n\n\t\tif (isAsyncFunction(route?.handler)) {\n\t\t\tawait route.handler({ route, params });\n\t\t}\n\t}\n\n\t/**\n\t *\n\t * @param {string} url - Current url users visite or nagivate to.\n\t * @returns {import('./types.js').MatchedRoute}\n\t */\n\t_matchUrlToRoute(url) {\n\t\t/** @type {import('./types.js').RouteParams} */\n\t\tconst params = {};\n\n\t\tif (url !== \"/\") {\n\t\t\turl = addTrailingSlash(url);\n\t\t}\n\n\t\tconst matchedRoute = Object.entries(this._routes).find(([route, routeObject]) => {\n\t\t\tif (url.split(\"/\").length !== route.split(\"/\").length) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst { types } = routeObject;\n\n\t\t\tlet routeSegments = route.split(\"/\").slice(1);\n\t\t\tlet urlSegments = url.split(\"/\").slice(1);\n\n\t\t\t// If each segment in the url matches the corresponding segment in the route path,\n\t\t\t// or the route path segment starts with a ':' then the route is matched.\n\t\t\tconst match = routeSegments.every((segment, i) => {\n\t\t\t\tif (!segment.startsWith(\":\")) {\n\t\t\t\t\treturn segment === urlSegments[i];\n\t\t\t\t}\n\n\t\t\t\t// /product/:id => segment/segment\n\t\t\t\t// /product/123 => urlSegment/urlSegment\n\t\t\t\tconst type = types[segment.replace(':', '')];\n\n\t\t\t\t// const segmentType = \n\t\t\t\tconst isValid = type === 'number'\n\t\t\t\t\t? Number.isInteger(Number(urlSegments[i]))\n\t\t\t\t\t: true;\n\n\t\t\t\treturn isValid;\n\t\t\t});\n\n\t\t\tif (!match) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// If the route matches the URL, pull out any params from the URL.\n\t\t\trouteSegments.forEach((segment, i) => {\n\t\t\t\tif (segment.startsWith(\":\")) {\n\t\t\t\t\tconst propName = segment.slice(1);\n\t\t\t\t\tparams[propName] = decodeURIComponent(urlSegments[i]);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\treturn true;\n\t\t});\n\n\t\tif (matchedRoute) {\n\t\t\treturn {\n\t\t\t\troute: this._routes[matchedRoute[0]],\n\t\t\t\tparams,\n\t\t\t\tsearchParams: new URLSearchParams(window.location.search),\n\t\t\t};\n\t\t} else {\n\t\t\treturn {\n\t\t\t\troute: this._routes[404],\n\t\t\t\tparams,\n\t\t\t\tsearchParams: null,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Handles <a> link clicks\n\t * @param {Event} event\n\t * @returns {void}\n\t */\n\t_handleClick(event) {\n\t\tconst anchor = findAnchor(event);\n\t\tif (!anchor) {\n\t\t\treturn;\n\t\t}\n\n\t\t/**\n\t\t * Allow users to define an `data-` attribute to skip the route handling.\n\t\t * Default to `data-breeze-ignore`.\n\t\t */\n\t\tif (anchor.hasAttribute(this.#ignoreAttribute)) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!shouldRouterHandleClick(event, anchor)) {\n\t\t\treturn;\n\t\t}\n\n\t\tevent.preventDefault();\n\t\tlet href = anchor.getAttribute(\"href\")?.trim();\n\t\tif (!href?.startsWith(\"/\")) {\n\t\t\thref = \"/\" + href;\n\t\t}\n\n\t\tthis.navigateTo(href);\n\t}\n\n\t/**\n\t * Add or remove search param to current url.\n\t * @param {HTMLInputElement} checkbox\n\t * @param {string} value\n\t * @returns void\n\t */\n\ttoggleParam(checkbox, value) {\n\t\tconst params = new URLSearchParams(location.search);\n\t\tconst name = checkbox.getAttribute(\"name\");\n\t\tif (!name) {\n\t\t\treturn console.warn(`name attribute is not set on ${checkbox.outerHTML}`);\n\t\t}\n\t\tif (checkbox.checked) {\n\t\t\t!params.has(name) && params.set(name, value);\n\t\t} else if (!checkbox.checked) {\n\t\t\tparams.has(name) && params.delete(name);\n\t\t}\n\n\t\tconst newUrl = !!params.size\n\t\t\t? `${location.pathname}?${params.toString()}`\n\t\t\t: location.pathname;\n\t\tthis.navigateTo(newUrl);\n\t}\n}\n"],"names":["addTrailingSlash","url","trim","endsWith","BreezeRouter","ignoreAttribute","constructor","config","this","_routes","_previousRoute","window","addEventListener","_onChanged","bind","document","body","_handleClick","start","add","route","handler","console","warn","error","types","includes","split","filter","segment","forEach","type","match","replace","propName","replaceAll","path","navigateTo","history","pushState","redirect","async","location","pathname","params","_matchUrlToRoute","_handleRoute","fn","name","toLowerCase","isAsyncFunction","matchedRoute","Object","entries","find","routeObject","length","routeSegments","slice","urlSegments","every","i","startsWith","Number","isInteger","decodeURIComponent","searchParams","URLSearchParams","search","event","anchor","composedPath","elem","tagName","hasAttribute","e","defaultPrevented","metaKey","ctrlKey","shiftKey","target","getAttribute","href","origin","shouldRouterHandleClick","preventDefault","toggleParam","checkbox","value","outerHTML","checked","has","set","delete","newUrl","size","toString"],"mappings":"AAOO,MAiCMA,EAAoBC,KAC/BA,EAAMA,EAAIC,QACDC,SAAS,OAChBF,GAAY,KAGPA,GCjCM,MAAMG,EAMpBC,GAAmB,qBAOnBC,YAAYC,GACX,MAAMF,gBAAEA,GAAoBE,GAAU,GAElCF,IACHG,MAAKH,EAAmBA,GAQzBG,KAAKC,QAAU,GAOfD,KAAKE,eAAiB,KAGtBC,OAAOC,iBAAiB,WAAYJ,KAAKK,WAAWC,KAAKN,OACzDO,SAASC,KAAKJ,iBAAiB,QAASJ,KAAKS,aAAaH,KAAKN,MAC/D,CAMDU,QACCV,KAAKK,YACL,CAQDM,IAAIC,EAAOC,GAMV,GAJc,OADdD,EAAQA,EAAMlB,UAEbkB,EAAQpB,EAAiBoB,IAGtBZ,KAAKC,QAAQW,GAChB,OAAOE,QAAQC,KAAK,yBAAyBH,KAG9C,GAAuB,mBAAZC,EACV,OAAOC,QAAQE,MAAM,qBAAqBJ,0BAG3C,MAAMK,EAAQ,CAAA,EAGd,GAAIL,EAAMM,SAAS,KAAM,CAEJN,EAAMO,MAAM,KAAKC,QAAOC,GAAWA,EAAQH,SAAS,OAG5DI,SAAQD,IAEnB,MAAME,EAAOF,EAAQG,MAAM,YAAY,GACrCC,QAAQ,IAAK,IACbA,QAAQ,IAAK,IAGTC,EAAWL,EAAQI,QAAQ,SAAU,IAAIA,QAAQ,IAAK,IAG5DR,EAAMS,GAAYH,CAAI,GAEvB,CAWD,OARAX,EAAQA,EAAMe,WAAW,UAAW,IAEpC3B,KAAKC,QAAQW,GAAS,CACrBgB,KAAMhB,EACNK,QACAJ,WAGMb,IACP,CAOD6B,WAAWpC,GACVU,OAAO2B,QAAQC,UAAU,CAAEtC,OAAO,GAAIA,GACtCO,KAAKK,YACL,CAOD2B,SAASvC,GACRO,KAAK6B,WAAWpC,EAChB,CAEDwC,mBACC,MAAML,EAAOzB,OAAO+B,SAASC,UACvBvB,MAAEA,EAAKwB,OAAEA,GAAWpC,KAAKqC,iBAAiBT,SAI1C5B,KAAKsC,aAAa,CAAE1B,QAAOwB,WAKjCpC,KAAKE,eAAiBU,CACtB,CAODqB,oBAAmBrB,MAAEA,EAAKwB,OAAEA,IDjJH,IAACG,ICkJV3B,GAAOC,QDjJjB0B,GACwC,aAAtCA,EAAGzC,YAAY0C,KAAKC,eCiJ1B7B,EAAMC,QAAQ,CAAED,QAAOwB,WDzIK,CAACG,KACzBA,GACwC,kBAAtCA,EAAGzC,YAAY0C,KAAKC,cC0IvBC,CAAgB9B,GAAOC,gBACpBD,EAAMC,QAAQ,CAAED,QAAOwB,UAE9B,CAODC,iBAAiB5C,GAEhB,MAAM2C,EAAS,CAAA,EAEH,MAAR3C,IACHA,EAAMD,EAAiBC,IAGxB,MAAMkD,EAAeC,OAAOC,QAAQ7C,KAAKC,SAAS6C,MAAK,EAAElC,EAAOmC,MAC/D,GAAItD,EAAI0B,MAAM,KAAK6B,SAAWpC,EAAMO,MAAM,KAAK6B,OAC9C,OAAO,EAGR,MAAM/B,MAAEA,GAAU8B,EAElB,IAAIE,EAAgBrC,EAAMO,MAAM,KAAK+B,MAAM,GACvCC,EAAc1D,EAAI0B,MAAM,KAAK+B,MAAM,GAqBvC,QAjBcD,EAAcG,OAAM,CAAC/B,EAASgC,KAC3C,IAAKhC,EAAQiC,WAAW,KACvB,OAAOjC,IAAY8B,EAAYE,GAYhC,MAJyB,WAHZpC,EAAMI,EAAQI,QAAQ,IAAK,MAIrC8B,OAAOC,UAAUD,OAAOJ,EAAYE,IAGzB,MAQfJ,EAAc3B,SAAQ,CAACD,EAASgC,KAC/B,GAAIhC,EAAQiC,WAAW,KAAM,CAC5B,MAAM5B,EAAWL,EAAQ6B,MAAM,GAC/Bd,EAAOV,GAAY+B,mBAAmBN,EAAYE,GAClD,MAGK,EAAI,IAGZ,OAAIV,EACI,CACN/B,MAAOZ,KAAKC,QAAQ0C,EAAa,IACjCP,SACAsB,aAAc,IAAIC,gBAAgBxD,OAAO+B,SAAS0B,SAG5C,CACNhD,MAAOZ,KAAKC,QAAQ,KACpBmC,SACAsB,aAAc,KAGhB,CAODjD,aAAaoD,GACZ,MAAMC,EAAoBD,EDzLjBE,eAAejB,MAAMkB,GACJ,MAAjBA,EAAKC,UCyLd,IAAKH,EACJ,OAOD,GAAIA,EAAOI,aAAalE,MAAKH,GAC5B,OAGD,ID3LqC,EAACsE,EAAGL,KAEzC,GAAIK,EAAEC,iBACJ,OAAO,EAIT,GAAID,EAAEE,SAAWF,EAAEG,SAAWH,EAAEI,SAC9B,OAAO,EAGT,IAAKT,EACH,OAAO,EAGT,GAAIA,EAAOU,OACT,OAAO,EAGT,GACEV,EAAOI,aAAa,aACW,aAA/BJ,EAAOW,aAAa,OAEpB,OAAO,EAGT,MAAMC,EAAOZ,EAAOY,KACpB,SAAKA,GAAQA,EAAKpB,WAAW,aAKxBoB,EAAKpB,WAAWpB,SAASyC,QAInB,ECuJNC,CAAwBf,EAAOC,GACnC,OAGDD,EAAMgB,iBACN,IAAIH,EAAOZ,EAAOW,aAAa,SAAS/E,OACnCgF,GAAMpB,WAAW,OACrBoB,EAAO,IAAMA,GAGd1E,KAAK6B,WAAW6C,EAChB,CAQDI,YAAYC,EAAUC,GACrB,MAAM5C,EAAS,IAAIuB,gBAAgBzB,SAAS0B,QACtCpB,EAAOuC,EAASN,aAAa,QACnC,IAAKjC,EACJ,OAAO1B,QAAQC,KAAK,gCAAgCgE,EAASE,aAE1DF,EAASG,SACX9C,EAAO+C,IAAI3C,IAASJ,EAAOgD,IAAI5C,EAAMwC,GAC3BD,EAASG,SACpB9C,EAAO+C,IAAI3C,IAASJ,EAAOiD,OAAO7C,GAGnC,MAAM8C,EAAWlD,EAAOmD,KACrB,GAAGrD,SAASC,YAAYC,EAAOoD,aAC/BtD,SAASC,SACZnC,KAAK6B,WAAWyD,EAChB"}
package/dist/types.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * @typedef {Object} MatchedRoute
13
13
  * @property {Route} route
14
14
  * @property {RouteParams} params
15
+ * @property {URLSearchParams|null} searchParams
15
16
  */
16
17
 
17
18
  export { Route, RouteParams, MatchedRoute };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "breeze-router",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "A lightweight, zero-dependency client-side router for single page applications (SPAs).",
5
5
  "main": "index.js",
6
6
  "type": "module",