From 5e08e98809d44b5b9d4bbeb20e14876a9d35586e Mon Sep 17 00:00:00 2001 From: Serhii Kostiukevych Date: Fri, 13 Jun 2025 17:27:30 +0300 Subject: [PATCH 1/3] Link click listeners --- src/client.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index 183ecc1..284d521 100644 --- a/src/client.js +++ b/src/client.js @@ -80,7 +80,7 @@ export function isLocalURL(url) { export function bootstrap() { console.assert( !!window, - "boostrap error: must be called from within a browser context", + "bootstrap error: must be called from within a browser context", ); const link = window.document.querySelector( 'head link[rel*="alternate"][type="application/vnd.api+json"]', @@ -93,7 +93,35 @@ export function bootstrap() { ) : new URL(link.getAttribute("href")); const client = new Client(initialURL.href); - // Register a global listener for history updates. + + // Add click event listener to div#app + const appDiv = document.getElementById("app"); + console.assert(!!appDiv, "bootstrap error: missing div#app element"); + appDiv.addEventListener("click", (event) => { + // Check if the target is an anchor element + if (!(event.target instanceof HTMLAnchorElement)) return; + + const anchor = event.target; + const href = anchor.href; // Safe to access href directly + + // Check if href is a relative URL + const isRelative = href && + (href.startsWith("./") || href.startsWith("/") || + !/^(?:[a-z]+:)?\/\//i.test(href)); + if (!isRelative) return; + + // Check if anchor has a type attribute (opt-out) + if (anchor.hasAttribute("type")) return; + + // Prevent default behavior and stop propagation + event.preventDefault(); + event.stopPropagation(); + + // Call the client's follow function + client.follow(href, {}); + }); + + // Register a global listener for history updates addEventListener("popstate", (event) => { // Not navigating without a state. if (event.state) { @@ -106,6 +134,7 @@ export function bootstrap() { } } }); + return client; } From a4c8772aa2687a922f16b9f5d42404631a91b079 Mon Sep 17 00:00:00 2001 From: Serhii Kostiukevych Date: Sat, 21 Jun 2025 01:31:20 +0300 Subject: [PATCH 2/3] Apply Gabe's and Peter's suggestions --- src/client.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index 284d521..41d50cc 100644 --- a/src/client.js +++ b/src/client.js @@ -104,21 +104,39 @@ export function bootstrap() { const anchor = event.target; const href = anchor.href; // Safe to access href directly - // Check if href is a relative URL - const isRelative = href && - (href.startsWith("./") || href.startsWith("/") || - !/^(?:[a-z]+:)?\/\//i.test(href)); - if (!isRelative) return; + // Check if href is external or has a non-web protocol + let url; + try { + url = new URL(href, appDiv.baseURI); + } catch { + console.warn(`Invalid href: ${href}`); + return; + } + const isExternal = url.protocol !== "http:" && url.protocol !== "https:" || + url.origin !== new URL(appDiv.baseURI).origin; + if (isExternal) return; // Check if anchor has a type attribute (opt-out) if (anchor.hasAttribute("type")) return; + // Check if anchor has a download attribute + if (anchor.hasAttribute("download")) return; + + // Check if anchor has a target attribute that is not _blank + if ( + anchor.hasAttribute("target") && + anchor.getAttribute("target") !== "_blank" + ) return; + // Prevent default behavior and stop propagation event.preventDefault(); event.stopPropagation(); + // Use normalized href + const normalizedHREF = url.href; + // Call the client's follow function - client.follow(href, {}); + client.follow(normalizedHREF, {}); }); // Register a global listener for history updates From 99a1c8b7a513a4c91321d4e9345cc57b1ee3ff58 Mon Sep 17 00:00:00 2001 From: Serhii Kostiukevych Date: Sat, 21 Jun 2025 01:34:25 +0300 Subject: [PATCH 3/3] Remove unneeded options --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 41d50cc..a7d5298 100644 --- a/src/client.js +++ b/src/client.js @@ -136,7 +136,7 @@ export function bootstrap() { const normalizedHREF = url.href; // Call the client's follow function - client.follow(normalizedHREF, {}); + client.follow(normalizedHREF); }); // Register a global listener for history updates