lightweight-router 1.0.0 → 1.0.1
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/.vscode/settings.json +11 -0
- package/README.md +13 -8
- package/build.js +51 -0
- package/dist/router.min.js +1 -0
- package/jest.config.js +8 -0
- package/jest.setup.js +32 -0
- package/package.json +15 -4
- package/src/__tests__/router.test.js +132 -0
- package/src/about.html +24 -0
- package/src/index.html +24 -0
- package/src/router.html +25 -0
- package/src/router.js +148 -54
- package/package.tmp.json +0 -0
- package/src/index.js +0 -7
- package/src/utils.js +0 -20
package/README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Lightweight Router
|
|
2
2
|
|
|
3
|
-
A lightweight client-side router with prefetching capabilities.
|
|
3
|
+
A lightweight client-side router with intelligent prefetching capabilities for modern web applications.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
- 🚀 Zero dependencies
|
|
8
|
+
- 🔄 Smooth client-side navigation
|
|
9
|
+
- 📥 Intelligent link prefetching
|
|
10
|
+
- 🎯 Multiple prefetching strategies
|
|
11
|
+
- 🔍 SEO-friendly
|
|
12
|
+
- 📱 Mobile-friendly with data-saver mode support
|
|
13
|
+
- ⚡ Automatic script execution
|
|
14
|
+
- 🎨 Built-in loading animations
|
|
10
15
|
|
|
11
|
-
##
|
|
16
|
+
## Installation
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
### NPM
|
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
|
+
(()=>{let c={},n=async()=>{var e=document.querySelector("router"),t=globalThis.location.pathname;let o=e.querySelector(`route[path="${t}"]`),n=(o||((o=document.createElement("route")).setAttribute("path",globalThis.location.pathname),e.appendChild(o)),document.body.classList.add("loading"),c[globalThis.location.href]);n||(n=await d(globalThis.location.href),c[globalThis.location.href]=n);var r,a=(new DOMParser).parseFromString(n,"text/html"),i=a.querySelector("title"),i=(i&&(document.title=i.textContent),o.innerHTML=a.body.innerHTML,Array.from(o.querySelectorAll("script")));for(r of i){var l=document.createElement("script");r.src?l.src=r.src:l.textContent=r.textContent,r.parentNode.replaceChild(l,r)}e.querySelectorAll("route").forEach(e=>e.style.display="none"),o.style.display="contents",document.body.classList.remove("loading"),window.scrollTo(0,0),s&&s(t)},r=async e=>{c[e.href]||(c[e.href]=await d(e.href))},a=(e,t)=>{e.forEach(e=>{e.isIntersecting&&(e=e.target,c[e.href]||(r(e),t.unobserve(e)))})},i=e=>{var t=e.target.closest("A");t&&t.href&&l(t.href)&&t.origin===location.origin&&(e.preventDefault(),globalThis.history.pushState(null,null,t.href),globalThis.dispatchEvent(new Event("popstate")))};function l(e){if(e&&!e.startsWith("#")&&!e.startsWith("javascript:")){if(e.startsWith("/"))return 1;try{var t=new URL(e,window.location.origin),o=new URL(window.location.href);return o.hostname.replace(/^www\./,"")===t.hostname.replace(/^www\./,"")?o.pathname!==t.pathname||!t.hash:void 0}catch{}}}let s,d=async e=>{e=await t(e);return e.ok?e.text():"Couldn't fetch the route - HTTP error! status: "+e.status},t=async e=>{if(!h){var t=await fetch(e,{method:"POST",body:"onlyRoute"});if(t.ok)return t}return fetch(e)},h=!1,e=(e={})=>{var t,e=e.onRouteChange,e=(e&&(e=e,s=e),document.createElement("style")),e=(e.textContent="body.loading{animation:pulseOpacity 1s infinite alternate}@keyframes pulseOpacity{from{opacity:.8}to{opacity:.3}}",document.head.appendChild(e),document.querySelector("router")),o=globalThis.location.pathname,o=(e||(e=document.createElement("router"),(t=document.createElement("route")).setAttribute("path",o),t.style.contentVisibility="auto",t.innerHTML=document.body.innerHTML,e.appendChild(t),document.body.innerHTML="",document.body.appendChild(e),h=!0),globalThis.addEventListener("popstate",n),document.addEventListener("click",i),document.body.addEventListener("mouseover",e=>{"A"===e.target.tagName&&"onHover"===e.target.getAttribute("prefetch")&&(async e=>{e=e.target;!c[e.href]&&l(e.href)&&await r(e)})(e)}),new IntersectionObserver(a,{root:null,threshold:.5}));(t=>{let o=navigator.connection&&navigator.connection.saveData;document.querySelectorAll("a").forEach(e=>{"onHover"===e.getAttribute("prefetch")||o||l(e.href)||t.observe(e)})})(o)};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>e()):e()})();
|
package/jest.config.js
ADDED
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.
|
|
4
|
-
"main": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"main": "dist/router.min.js",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"
|
|
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>
|
package/src/router.html
ADDED
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
}
|
|
16
|
-
});
|
|
40
|
+
oldScript.parentNode.replaceChild(newScript, oldScript);
|
|
41
|
+
}
|
|
17
42
|
|
|
18
|
-
|
|
43
|
+
router.querySelectorAll("route").forEach(route => (route.style.display = "none"));
|
|
44
|
+
currentRoute.style.display = "contents";
|
|
19
45
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
46
|
+
document.body.classList.remove("loading");
|
|
47
|
+
// Reset scroll position to the top
|
|
48
|
+
window.scrollTo(0, 0);
|
|
23
49
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
47
|
-
const
|
|
48
|
-
if (!
|
|
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 =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
if (
|
|
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",
|
|
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
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
|
-
};
|