lightweight-router 1.0.0 → 1.0.2

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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "files.exclude": {
3
+ "**/.git": true,
4
+ "**/.svn": true,
5
+ "**/.hg": true,
6
+ "**/CVS": true,
7
+ "**/.DS_Store": true,
8
+ "**/Thumbs.db": true
9
+ },
10
+ "hide-files.files": []
11
+ }
package/README.md CHANGED
@@ -1,13 +1,159 @@
1
- # lightweight-router
1
+ # Lightweight Router
2
+
3
+ A minimal lightweight client-side router with intelligent prefetching capabilities for faster websites. This tool can turn any Multi-Page Application (MPA) into a Single-Page Application (SPA) very easily and with just ~1.5KB byte (gzipped).
4
+
5
+ ## Features
6
+
7
+ - 🚀 Zero dependencies
8
+ - 🔄 Smooth client-side navigation
9
+ - 📥 Intelligent link prefetching
10
+ - 🎯 Multiple prefetching strategies
11
+ - 🔍 SEO-friendly (works with Wordpress)
12
+ - 📱 Mobile-friendly with data-saver mode support
13
+ - 🎨 Built-in loading animations
14
+ - 🕰️ Based on History API so you can use native browser navigation
2
15
 
3
- A lightweight client-side router with prefetching capabilities.
4
16
 
5
17
  ## Installation
6
18
 
7
- ```bash
19
+ ### NPM
20
+
21
+ ```sh
8
22
  npm install lightweight-router
9
23
  ```
10
24
 
11
25
  ## Usage
12
26
 
13
- Refer to the documentation for more details.
27
+ To use the lightweight router in your project, follow these steps:
28
+
29
+ 1. Import the `startRouter` function from the router module.
30
+ 2. Call the `startRouter` function to initialize the router.
31
+
32
+ Example:
33
+
34
+ ```javascript
35
+ import { startRouter } from "lightweight-router";
36
+
37
+ startRouter({
38
+ onRouteChange: currentRoute => {
39
+ console.log("Route changed:", currentRoute);
40
+ },
41
+ });
42
+ ```
43
+
44
+ ### Direct Import
45
+
46
+ You can also directly import the minified version of the router in your HTML file or paste its content inside a script tag:
47
+
48
+ ```html
49
+ <!DOCTYPE html>
50
+ <html lang="en">
51
+ <head>
52
+ <meta charset="UTF-8" />
53
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
54
+ <title>My App</title>
55
+ </head>
56
+ <body>
57
+ <!-- Your website content -->
58
+ <script src="path/to/dist/router.min.js"></script>
59
+ </body>
60
+ </html>
61
+ ```
62
+
63
+ ## API
64
+
65
+ ### `startRouter(options)`
66
+
67
+ Initializes the router with the given options.
68
+
69
+ #### Parameters
70
+
71
+ - `options` (Object): Configuration options for the router.
72
+ - `onRouteChange` (Function): Callback function to be called when the route changes.
73
+
74
+ ## Examples
75
+
76
+ ### Basic Example
77
+
78
+ ```html
79
+ Your website content
80
+ <script type="module">
81
+ import { startRouter } from "./router.js";
82
+
83
+ startRouter({
84
+ onRouteChange: currentRoute => {
85
+ console.log("Route changed:", currentRoute);
86
+ },
87
+ });
88
+ </script>
89
+ ```
90
+
91
+ ## Server Configuration
92
+
93
+ Configuring your server to return only the route content can make the router much more efficient. Instead of returning the entire page, the server should return only the content for the requested route when it detects a request with the message "onlyRoute".
94
+
95
+ ```javascript
96
+ await fetch(url, { method: "POST", body: "onlyRoute" });
97
+ ```
98
+
99
+ This allows only the changing part of the document to be updated, improving performance and reducing bandwidth usage.
100
+
101
+ Once you configured your server to respond to this type of request, wrap the part of your document that changes in a `router` tag. Inside the `router` tag, render the current initial route inside a `route` tag like this:
102
+
103
+ ```html
104
+ <-- Header menu and parts that don't change -->
105
+ <router>
106
+ <route path="/" style="content-visibility: auto">home content</route>
107
+ </router>
108
+ <-- footer etc.. -->
109
+ ```
110
+
111
+ You can also prerender other important routes by rendering them inside the `router` tag in their appropriate `route` tags for faster loading times:
112
+
113
+ ```html
114
+ <router>
115
+ <route path="/" style="content-visibility: auto">home content</route>
116
+ <route path="/about" style="content-visibility: auto; display:none;">about content</route>
117
+ </router>
118
+ ```
119
+
120
+ In the future you will also be able to pre-render a default route that will be used as 404 by having it at /404 or /default
121
+
122
+ Right now errors are shown without styling as the content of the page.
123
+
124
+ Soon there will be a DenoJS library that will help you deal with all these routes stuff. It will also come with api routes functionality 🔥
125
+
126
+ ## Prefetching
127
+
128
+ By default, links are prefetched when they get in the user's screen using an `IntersectionObserver`. This ensures that the content is loaded in the background before the user clicks on the link, providing a smoother navigation experience.
129
+
130
+ If you have too many links at once or too many requests, you can add the `prefetch="onHover"` attribute to your links or some of them (usually links to huge pages that are not often visited):
131
+
132
+ ```html
133
+ <a href="/archive" prefetch="onHover">Archive</a>
134
+ ```
135
+
136
+ P.S. you can easily test in your website by pasting this minified version into the console.
137
+
138
+ The minified version was created with uglify-js, clean.css and then minified again with https://packjs.com
139
+ The size of the gzipped version was calculated with: https://dafrok.github.io/gzip-size-online/
140
+ It's worth to note that nonethewise Terser give better results than uglify-js. The final uglify version packed by packjs.com was even smaller.
141
+
142
+ ## Browser Support
143
+
144
+ The router supports all modern browsers. Required features:
145
+
146
+ - IntersectionObserver
147
+ - Fetch API
148
+ - History API
149
+
150
+ For older browsers, consider using the following polyfills:
151
+
152
+ - intersection-observer
153
+ - whatwg-fetch
154
+
155
+ ## Performance Tips
156
+
157
+ - Use `content-visibility: auto` on route elements to improve rendering performance
158
+ - Implement server-side partial responses for better bandwidth usage
159
+ - Consider using the `prefetch="onHover"` attribute for less important links
package/build.js ADDED
@@ -0,0 +1,51 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const uglifyJS = require("uglify-js");
4
+ const CleanCSS = require("clean-css");
5
+
6
+ // Read the router source
7
+ const routerSource = fs.readFileSync(path.join(__dirname, "src/router.js"), "utf8");
8
+
9
+ // Extract CSS from the source
10
+ const cssRegex = /style\.textContent = `\s*([\s\S]*?)\s*`;/;
11
+ const cssMatch = routerSource.match(cssRegex);
12
+ const css = cssMatch ? cssMatch[1] : "";
13
+
14
+ // Minify CSS
15
+ const minifiedCSS = new CleanCSS({}).minify(css).styles;
16
+
17
+ // Replace original CSS with minified version
18
+ const updatedSource = routerSource.replace(cssRegex, `style.textContent = '${minifiedCSS}';`);
19
+
20
+ // Convert ES module to self-initializing IIFE
21
+ const iife = `
22
+ (function() {
23
+ ${updatedSource.replace("export { startRouter };", "")}
24
+
25
+ // Auto-initialize when DOM is ready
26
+ if (document.readyState === 'loading') {
27
+ document.addEventListener('DOMContentLoaded', () => startRouter());
28
+ } else {
29
+ startRouter();
30
+ }
31
+ })();
32
+ `;
33
+
34
+ // Minify the JavaScript
35
+ const minified = uglifyJS.minify(iife);
36
+
37
+ if (minified.error) {
38
+ console.error("Minification error:", minified.error);
39
+ process.exit(1);
40
+ }
41
+
42
+ // Create dist directory if it doesn't exist
43
+ const distDir = path.join(__dirname, "dist");
44
+ if (!fs.existsSync(distDir)) {
45
+ fs.mkdirSync(distDir);
46
+ }
47
+
48
+ // Write minified file
49
+ fs.writeFileSync(path.join(distDir, "router.min.js"), minified.code);
50
+
51
+ console.log("Build complete! Output saved to dist/router.min.js");
@@ -0,0 +1 @@
1
+ $="(£ì­c={},n=async£ìܵ¨¢t=êð;­¦úè(`ä[pÓh=${t}]`÷n=(oÊ((o´êð÷ú½(o)÷Æç.addöÛ³¼nÊ(n=×d(êó³=n¼Ür,a=(ÍDOMParsõ).parseF°mString(n,ýxt/html¢i=a.èötàle¢i=(iÞ(¾tàlµi.Ý÷¥î=a.¬.î,Array.f°m(¥¹Éé)¼for(r of i){Ül=ßÉé;r«?l«=r«:l.Ý=r.Ý,r.paùïNodÐChild(l,rØú¹äé.fÔ=>ú²nëeé÷¥²c¡sÑÆç.ùmo·öÛ¢Ïsc°llTo(0,0÷sÞs(tØ,r=âËÊ(Ë=×dªØ,a=ô,t)ìúfÔìúisIÀngÞôí,ËÊ(rô÷Õun®))±Ø,i=eìÜtí.closeçöAé;tÞÕóÞl(Õó)ÞÕÈÒ=lüÈÞ(úpù·ïDefault(ºhiçory.pushStaý(¸¸ÕóºdispÓchE·ï(ÍE·ïö¯é)Ø;functië lô){ifô©#é©javaÉ:é){ Öhö/é)á1;try{Üt=ÍURLô,ÏlüÈ÷¦ÍURL(Ïlüó¼á¥¤Ò=Õ¤?¥ð!ÒÕðÊ!Õhash:void 0}cÓch{}}}­s,d=âµ×tô¼áúok?úýxt£:Couldn't Ì the ä - HTTP õ°r! çÓus: +úçÓus},t=â !h){Üt=×Ìô,{mÚhod:POSTѬ:ëlyRouý}¼ Õok)át}áÌôØ,h=!1,µô={})ìÜt,µúëRouýChange,µôÞô=e,s=e÷ßçyleé÷µ(úÝ=¬.Û{animÓië:¶ 1s infiniý alýrnaý}@keyframes ¶{f°m§8}to§3}}Ѿhead.½ô÷¨é÷¦êð,¦ôÊô=ßär¢(t´o÷Õçylúc¡Visibilày=autoÑÕî=òî,ú½(t÷òî=Ñò½ô÷h=!0ºÙ¯Ñn÷¾ÙclickÑi÷òÙmouseovõÑôìAÒí.tagNameÞ»í.¿Þ(âeí;!ËÞlªÞ×rô±ô±÷ÍIÀëObsõvõ(a,{°ot:¸thùshold:.5})¼(tì­¦naÁÞnaÁ.sa·DÓa;¾¹aé.fÔì»=ú¿ÊoÊlªÊÕ®±±(oØ;ÛÒ=¾ùadyStaý?¾ÙDOMC¡LoadedÑ(£=>e£)):e(±(¼";l="if(ëýï÷()hoÂéo.o={oÎ:.¾èöärÞ!Öhö(úó).srcbodylÚ obsõ·ôpopçaýroØ)çylÇ=÷c[êó]=ßäÅÓhÑe=pulseOÎvenull,èAllö÷glû.ëHovõÒ);aãlddþgÚAåpùÌénýrsectivigÃctiëÄwww./,Óor.cëneçnamÐ(/^é).sÚAåpòclassLiúdisplayoriginscript||c[úó]fÚchnew pacàywindow.úùplace,==atorEach(ôt.úçartsWàawaà )}addEveæetloadingvar ýxtCëýï&&dñmeïöitùturn async eìppendChirouýttribuýöïLisýnõöstquõør)glû.lüon=>{=útargetinnõHTMLntpathnameþcùaýEledþbody.hùf(eer(),ySelectoree.obalThisocation.teocument.".split('');_=" ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ";for(i=0;i<95;i++){$=$.split(_[i]).join(l[i])};eval($.replace(//g,'"').replace(//g,'\\').replace(//g,String.fromCharCode(10)));
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ testEnvironment: "jsdom",
3
+ moduleFileExtensions: ["js", "jsx"],
4
+ transform: {},
5
+ testMatch: ["**/__tests__/**/*.test.js"],
6
+ setupFiles: ["<rootDir>/jest.setup.js"],
7
+ moduleDirectories: ["node_modules", "dist"],
8
+ };
package/jest.setup.js ADDED
@@ -0,0 +1,32 @@
1
+ // Mock window.scrollTo
2
+ window.scrollTo = jest.fn();
3
+
4
+ // Mock IntersectionObserver
5
+ global.IntersectionObserver = class IntersectionObserver {
6
+ constructor(callback) {
7
+ this.callback = callback;
8
+ }
9
+
10
+ observe = jest.fn();
11
+ unobserve = jest.fn();
12
+ disconnect = jest.fn();
13
+ };
14
+
15
+ // Mock URL API if needed
16
+ global.URL = class URL {
17
+ constructor(url) {
18
+ this.url = url;
19
+ this.pathname = url;
20
+ this.origin = "http://localhost";
21
+ this.hash = "";
22
+ this.hostname = "localhost";
23
+ }
24
+ };
25
+
26
+ // Mock window.location
27
+ const mockLocation = new URL("http://localhost/");
28
+ delete window.location;
29
+ window.location = mockLocation;
30
+
31
+ // Mock history API
32
+ window.history.pushState = jest.fn();
package/package.json CHANGED
@@ -1,12 +1,23 @@
1
1
  {
2
2
  "name": "lightweight-router",
3
- "version": "1.0.0",
4
- "main": "index.js",
3
+ "version": "1.0.2",
4
+ "main": "dist/router.min.js",
5
5
  "scripts": {
6
- "test": "echo \"Error: no test specified\" && exit 1"
6
+ "dev": "npx http-server src -o",
7
+ "build": "node build.js",
8
+ "deploy": "npm run build && npm version patch && git publish && npm publish",
9
+ "test": "jest",
10
+ "test:watch": "jest --watch"
7
11
  },
8
12
  "keywords": [],
9
13
  "author": "",
10
14
  "license": "ISC",
11
- "description": ""
15
+ "description": "A lightweight client-side router with prefetching capabilities.",
16
+ "devDependencies": {
17
+ "http-server": "^14.1.1",
18
+ "uglify-js": "^3.17.4",
19
+ "clean-css": "^5.3.1",
20
+ "jest": "^29.7.0",
21
+ "jest-environment-jsdom": "^29.7.0"
22
+ }
12
23
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ describe("Router", () => {
6
+ beforeEach(async () => {
7
+ // Setup a clean DOM environment before each test
8
+ document.body.innerHTML = `
9
+ <div>
10
+ <h1>Test Page</h1>
11
+ <a href="/about">About</a>
12
+ <a href="/contact" prefetch="onHover">Contact</a>
13
+ </div>
14
+ `;
15
+
16
+ // Mock fetch API
17
+ global.fetch = jest.fn(() =>
18
+ Promise.resolve({
19
+ ok: true,
20
+ text: () => Promise.resolve("<div>New Content</div>"),
21
+ })
22
+ );
23
+
24
+ // Load the router after DOM is set up
25
+ require("../../dist/router.min.js");
26
+
27
+ // Wait for next tick to allow router initialization
28
+ await new Promise(resolve => setTimeout(resolve, 0));
29
+ });
30
+
31
+ afterEach(() => {
32
+ jest.clearAllMocks();
33
+ // Clear the require cache so we can reload the router in next test
34
+ jest.resetModules();
35
+ });
36
+
37
+ test("initializes router and wraps existing content", async () => {
38
+ const router = document.querySelector("router");
39
+ const route = document.querySelector("route");
40
+
41
+ expect(router).toBeTruthy();
42
+ expect(route).toBeTruthy();
43
+ expect(route.getAttribute("path")).toBe(window.location.pathname);
44
+ });
45
+
46
+ test("handles internal navigation", async () => {
47
+ // Mock the window.location object
48
+ delete window.location;
49
+ window.location = new URL("http://localhost/");
50
+
51
+ // Mock history.pushState to actually update the location
52
+ const originalPushState = window.history.pushState;
53
+ window.history.pushState = jest.fn((state, title, url) => {
54
+ window.location = new URL(url, "http://localhost");
55
+ originalPushState.call(window.history, state, title, url);
56
+ });
57
+
58
+ await new Promise(resolve => setTimeout(resolve, 0));
59
+
60
+ const link = document.querySelector('a[href="/about"]');
61
+ link.click();
62
+
63
+ // Wait for async operations
64
+ await new Promise(resolve => setTimeout(resolve, 0));
65
+
66
+ expect(window.location.pathname).toBe("/about");
67
+ expect(fetch).toHaveBeenCalledWith("/about", expect.any(Object));
68
+ });
69
+
70
+ test("prefetches content on hover for marked links", async () => {
71
+ await new Promise(resolve => setTimeout(resolve, 0));
72
+
73
+ const link = document.querySelector('a[prefetch="onHover"]');
74
+ link.dispatchEvent(new MouseEvent("mouseover"));
75
+
76
+ // Wait for async operations
77
+ await new Promise(resolve => setTimeout(resolve, 0));
78
+
79
+ expect(fetch).toHaveBeenCalledWith("/contact", expect.any(Object));
80
+ });
81
+
82
+ test("updates document title when navigating", async () => {
83
+ await new Promise(resolve => setTimeout(resolve, 0));
84
+
85
+ // Mock fetch to return content with a title
86
+ global.fetch = jest.fn(() =>
87
+ Promise.resolve({
88
+ ok: true,
89
+ text: () =>
90
+ Promise.resolve(`
91
+ <html>
92
+ <head>
93
+ <title>New Page</title>
94
+ </head>
95
+ <body>
96
+ <div>New Content</div>
97
+ </body>
98
+ </html>
99
+ `),
100
+ })
101
+ );
102
+
103
+ const link = document.querySelector('a[href="/about"]');
104
+ link.click();
105
+
106
+ // Wait for async operations
107
+ await new Promise(resolve => setTimeout(resolve, 0));
108
+
109
+ expect(document.title).toBe("New Page");
110
+ });
111
+
112
+ test("handles navigation errors gracefully", async () => {
113
+ await new Promise(resolve => setTimeout(resolve, 0));
114
+
115
+ // Mock fetch to simulate an error
116
+ global.fetch = jest.fn(() =>
117
+ Promise.resolve({
118
+ ok: false,
119
+ status: 404,
120
+ })
121
+ );
122
+
123
+ const link = document.querySelector('a[href="/about"]');
124
+ link.click();
125
+
126
+ // Wait for async operations
127
+ await new Promise(resolve => setTimeout(resolve, 0));
128
+
129
+ const route = document.querySelector('route[path="/about"]');
130
+ expect(route.innerHTML).toContain("Couldn't fetch the route - HTTP error! status: 404");
131
+ });
132
+ });
package/src/about.html ADDED
@@ -0,0 +1,24 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="utf-8" />
4
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
5
+ <title>Router manual test About</title>
6
+ </head>
7
+ <body style="background-color: bisque">
8
+ <menu class="flex m-0 p-0"
9
+ ><a prefetch="onHover" href="/">Home</a><a href="/about.html">About</a><a href="/admin" prefetch="onHover">Admin</a
10
+ ><a href="/irmfirmiror" prefetch="onHover">404</a> <a href="https://andreafuturi.com" prefetch="onHover">Google Test</a>
11
+ </menu>
12
+ ABOUT
13
+ <script type="module">
14
+ import { startRouter } from "./router.js";
15
+ // Initialize the router with a simple onRouteChange callback to test functionality
16
+ startRouter({
17
+ onRouteChange: currentRoute => {
18
+ console.log("Route changed:", currentRoute);
19
+ currentRoute.classList.add("fade-in"); // Example: simple CSS class for animation (define it in CSS if needed)
20
+ },
21
+ });
22
+ </script>
23
+ </body>
24
+ </html>
package/src/index.html ADDED
@@ -0,0 +1,24 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="utf-8" />
4
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
5
+ <title>Router manual test Index</title>
6
+ </head>
7
+ <body>
8
+ <menu class="flex m-0 p-0"
9
+ ><a prefetch="onHover" href="/">Home</a><a preFetch="onHover" href="/about.html">About</a><a href="/admin" prefetch="onHover">Admin</a
10
+ ><a href="/irmfirmiror" prefetch="onHover">404</a> <a href="https://andreafuturi.com" prefetch="onHover">Google Test</a>
11
+ </menu>
12
+ HOME
13
+ <script type="module">
14
+ import { startRouter } from "./router.js";
15
+ // Initialize the router with a simple onRouteChange callback to test functionality
16
+ startRouter({
17
+ onRouteChange: currentRoute => {
18
+ console.log("Route changed:", currentRoute);
19
+ currentRoute.classList.add("fade-in"); // Example: simple CSS class for animation (define it in CSS if needed)
20
+ },
21
+ });
22
+ </script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,25 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="utf-8" />
4
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
5
+ <title>Router manual test About</title>
6
+ </head>
7
+ <body style="background-color: bisque">
8
+ <menu class="flex m-0 p-0"
9
+ ><a prefetch="onHover" href="/">Home</a><a href="/about.html">About</a><a href="/admin" prefetch="onHover">Admin</a
10
+ ><a href="/irmfirmiror" prefetch="onHover">404</a> <a href="https://andreafuturi.com" prefetch="onHover">Google Test</a> </menu
11
+ ><router>
12
+ <route path="/about.html" style="content-visibility: auto"><h1>About</h1></route><route path="/onlyServer" style="content-visibility: auto"></route
13
+ ></router>
14
+ <script type="module">
15
+ import { startRouter } from "./router.js";
16
+ // Initialize the router with a simple onRouteChange callback to test functionality
17
+ startRouter({
18
+ onRouteChange: currentRoute => {
19
+ console.log("Route changed:", currentRoute);
20
+ currentRoute.classList.add("fade-in"); // Example: simple CSS class for animation (define it in CSS if needed)
21
+ },
22
+ });
23
+ </script>
24
+ </body>
25
+ </html>
package/src/router.js CHANGED
@@ -1,34 +1,62 @@
1
- import { fetchContent } from './utils';
2
-
3
- let linkData = new Proxy({}, {
4
- set: (target, property, value) => {
5
- target[property] = value;
6
- const router = document.querySelector("router");
7
- const currentRoute = router?.querySelector(`route[path="${globalThis.location.pathname}"]`) ||
8
- router?.querySelector(`route[path="/default"]`);
9
-
10
- if (currentRoute && !currentRoute.innerHTML && property === globalThis.location.href) {
11
- currentRoute.innerHTML = value;
12
- if (typeof onRouteChange === 'function') onRouteChange(currentRoute);
1
+ let linkData = {};
2
+ const handlePopState = async () => {
3
+ const router = document.querySelector("router");
4
+ const currentPath = globalThis.location.pathname;
5
+
6
+ let currentRoute = router.querySelector(`route[path="${currentPath}"]`);
7
+
8
+ if (!currentRoute) {
9
+ currentRoute = document.createElement("route");
10
+ currentRoute.setAttribute("path", globalThis.location.pathname);
11
+ router.appendChild(currentRoute);
12
+ }
13
+
14
+ document.body.classList.add("loading");
15
+
16
+ let content = linkData[globalThis.location.href];
17
+ if (!content) {
18
+ content = await fetchContent(globalThis.location.href);
19
+ linkData[globalThis.location.href] = content;
20
+ }
21
+
22
+ const parser = new DOMParser();
23
+ const doc = parser.parseFromString(content, "text/html");
24
+
25
+ // Update the page title with the new content's title
26
+ const newTitle = doc.querySelector("title");
27
+ if (newTitle) document.title = newTitle.textContent;
28
+
29
+ currentRoute.innerHTML = doc.body.innerHTML;
30
+
31
+ // Execute scripts from the fetched content
32
+ const scripts = Array.from(currentRoute.querySelectorAll("script"));
33
+ for (const oldScript of scripts) {
34
+ const newScript = document.createElement("script");
35
+ if (oldScript.src) {
36
+ newScript.src = oldScript.src;
37
+ } else {
38
+ newScript.textContent = oldScript.textContent;
13
39
  }
14
- return true;
15
- },
16
- });
40
+ oldScript.parentNode.replaceChild(newScript, oldScript);
41
+ }
17
42
 
18
- let onRouteChange;
43
+ router.querySelectorAll("route").forEach(route => (route.style.display = "none"));
44
+ currentRoute.style.display = "contents";
19
45
 
20
- export const setRouteChangeHandler = (handler) => {
21
- onRouteChange = handler;
22
- };
46
+ document.body.classList.remove("loading");
47
+ // Reset scroll position to the top
48
+ window.scrollTo(0, 0);
23
49
 
24
- const fetchAndSaveContent = async (link) => {
25
- linkData[link.href] = "";
26
- linkData[link.href] = await fetchContent(link.href);
50
+ // Call the route change handler if it's set
51
+ if (onRouteChange) onRouteChange(currentPath);
27
52
  };
28
53
 
29
- const handleLinkHover = async (event) => {
30
- const link = event.target;
31
- if (!linkData[link.href]) await fetchAndSaveContent(link);
54
+ //link management
55
+
56
+ const fetchAndSaveContent = async link => {
57
+ if (!linkData[link.href]) {
58
+ linkData[link.href] = await fetchContent(link.href);
59
+ }
32
60
  };
33
61
 
34
62
  const handleLinkIntersection = (entries, observer) => {
@@ -43,49 +71,113 @@ const handleLinkIntersection = (entries, observer) => {
43
71
  });
44
72
  };
45
73
 
46
- const handlePopState = async () => {
47
- const router = document.querySelector("router");
48
- if (!router) return;
49
-
50
- const currentRoute = router.querySelector(`route[path="${globalThis.location.pathname}"]`) ||
51
- router.querySelector(`route[path="/default"]`);
52
-
53
- router.querySelectorAll("route").forEach(route => (route.style.display = "none"));
54
- currentRoute.style.display = "contents";
55
-
56
- if (!currentRoute.innerHTML) {
57
- currentRoute.innerHTML = await linkData[globalThis.location.href];
58
- if (typeof onRouteChange === 'function') onRouteChange(currentRoute);
74
+ const handleLinkHover = async event => {
75
+ const link = event.target;
76
+ if (!linkData[link.href] && isInternalLink(link.href)) {
77
+ await fetchAndSaveContent(link);
59
78
  }
60
79
  };
61
80
 
62
- const handleLinkClick = (e) => {
63
- if (e.target.tagName === "A" && e.target.href) {
64
- const href = new URL(e.target.href).pathname;
65
- if (href.startsWith("/")) {
66
- e.preventDefault();
67
- globalThis.history.pushState(null, null, e.target.href);
68
- globalThis.dispatchEvent(new Event("popstate"));
69
- }
70
- }
81
+ const handleLinkClick = e => {
82
+ const link = e.target.closest("A");
83
+ if (!link || !link.href || !isInternalLink(link.href) || link.origin !== location.origin) return;
84
+ else e.preventDefault();
85
+ globalThis.history.pushState(null, null, link.href);
86
+ globalThis.dispatchEvent(new Event("popstate"));
71
87
  };
72
88
 
73
- export const observeLinks = (observer) => {
89
+ const observeLinks = observer => {
74
90
  const saveDataOn = navigator.connection && navigator.connection.saveData;
75
91
  const links = document.querySelectorAll("a");
76
-
92
+
77
93
  links.forEach(link => {
78
- if (link.getAttribute("prefetch") !== "onHover" && !saveDataOn) observer.observe(link);
94
+ if (link.getAttribute("prefetch") !== "onHover" && !saveDataOn && !isInternalLink(link.href)) observer.observe(link);
79
95
  });
80
96
  };
81
97
 
82
- export const initializeRouter = () => {
83
- if (typeof document === "undefined") return;
98
+ function isInternalLink(href) {
99
+ if (!href || href.startsWith("#") || href.startsWith("javascript:")) return false;
100
+ if (href.startsWith("/")) return true;
101
+
102
+ try {
103
+ const url = new URL(href, window.location.origin);
104
+ const currentUrl = new URL(window.location.href);
105
+
106
+ const currentHost = currentUrl.hostname.replace(/^www\./, "");
107
+ const targetHost = url.hostname.replace(/^www\./, "");
108
+
109
+ // Compare hosts
110
+ if (currentHost !== targetHost) return false;
111
+
112
+ // Compare paths (ignoring parameters and fragments)
113
+ const currentPath = currentUrl.pathname;
114
+ const targetPath = url.pathname;
115
+
116
+ return currentPath !== targetPath || !url.hash;
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+
122
+ let onRouteChange;
123
+
124
+ const setRouteChangeHandler = handler => {
125
+ onRouteChange = handler;
126
+ };
127
+
128
+ const fetchContent = async url => {
129
+ const response = await fetchWithFallback(url);
130
+ if (!response.ok) {
131
+ return `Couldn't fetch the route - HTTP error! status: ${response.status}`;
132
+ }
133
+ return await response.text();
134
+ };
135
+
136
+ // Updated fetchWithFallback to check the flag
137
+ const fetchWithFallback = async url => {
138
+ if (!routerCreatedManually) {
139
+ const res = await fetch(url, { method: "POST", body: "onlyRoute" });
140
+ if (res.ok) return res;
141
+ }
142
+ return await fetch(url);
143
+ };
144
+
145
+ let routerCreatedManually = false;
146
+ const startRouter = (options = {}) => {
147
+ const { onRouteChange } = options;
148
+ if (onRouteChange) setRouteChangeHandler(onRouteChange);
149
+ const style = document.createElement("style");
150
+ style.textContent = `
151
+ body.loading {
152
+ animation: pulseOpacity 1s infinite alternate;
153
+ }
154
+
155
+ @keyframes pulseOpacity {
156
+ from { opacity: 0.8; }
157
+ to { opacity: 0.3; }
158
+ }
159
+ `;
160
+ document.head.appendChild(style);
161
+
162
+ let router = document.querySelector("router");
163
+ const currentPath = globalThis.location.pathname;
164
+
165
+ if (!router) {
166
+ router = document.createElement("router");
167
+ const route = document.createElement("route");
168
+ route.setAttribute("path", currentPath);
169
+ route.style.contentVisibility = "auto";
170
+ route.innerHTML = document.body.innerHTML;
171
+ router.appendChild(route);
172
+ document.body.innerHTML = "";
173
+ document.body.appendChild(router);
174
+ routerCreatedManually = true;
175
+ }
84
176
 
85
177
  globalThis.addEventListener("popstate", handlePopState);
86
178
  document.addEventListener("click", handleLinkClick);
87
-
88
- document.body.addEventListener("mouseover", (event) => {
179
+
180
+ document.body.addEventListener("mouseover", event => {
89
181
  if (event.target.tagName === "A" && event.target.getAttribute("prefetch") === "onHover") {
90
182
  handleLinkHover(event);
91
183
  }
@@ -94,3 +186,5 @@ export const initializeRouter = () => {
94
186
  const observer = new IntersectionObserver(handleLinkIntersection, { root: null, threshold: 0.5 });
95
187
  observeLinks(observer);
96
188
  };
189
+
190
+ export { startRouter };
package/package.tmp.json DELETED
File without changes
package/src/index.js DELETED
@@ -1,7 +0,0 @@
1
- import { initializeRouter, setRouteChangeHandler } from './router';
2
-
3
- export const startRouter = (options = {}) => {
4
- const { onRouteChange } = options;
5
- if (onRouteChange) setRouteChangeHandler(onRouteChange);
6
- initializeRouter();
7
- };
package/src/utils.js DELETED
@@ -1,20 +0,0 @@
1
- export const getPathname = () => {
2
- const { pathname } = globalThis.location;
3
- return pathname.startsWith("/") ? pathname.slice(1) : pathname;
4
- };
5
-
6
- export const fetchContent = async (url) => {
7
- const response = await fetch(url, { method: "POST", body: "onlyRoute" });
8
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
9
-
10
- const reader = response.body.getReader();
11
- const decoder = new TextDecoder("utf-8");
12
- let content = "";
13
-
14
- while (true) {
15
- const { done, value } = await reader.read();
16
- if (done) break;
17
- content += decoder.decode(value);
18
- }
19
- return content;
20
- };