From d23d5bc6dae8035357f1dea088392ab3d2546f8e Mon Sep 17 00:00:00 2001 From: tdro Date: Mon, 12 Feb 2024 20:15:17 -0500 Subject: static/js: Add custom DOM filter Fallback to native navigation if transition in 300ms is not guaranteed --- static/js/domfilter.ts | 145 +++++++++++++++++++++++++++++++++++++++++++++++++ static/js/index.ts | 1 + 2 files changed, 146 insertions(+) create mode 100644 static/js/domfilter.ts (limited to 'static/js') diff --git a/static/js/domfilter.ts b/static/js/domfilter.ts new file mode 100644 index 0000000..a81529b --- /dev/null +++ b/static/js/domfilter.ts @@ -0,0 +1,145 @@ +/** + * DOM Filter Copyright (C) 2024 Thedro Neely + * License: AGPL | https://www.gnu.org/licenses/agpl-3.0.txt + */ + +(function () { + type millisecond = number; + const timeout: millisecond = 300; + + const state = "on"; + const key = "config.speed.fast"; + + function fetch(url, method, callback) { + const http = new XMLHttpRequest(); + http.onreadystatechange = function () { + if (callback && http.readyState === 4) { + if (http.status === 200) callback(http); + else { + console.error("ERROR: Unable to navigate", http); + self.location.href = url; + } + } + }; + http.open(method, url); + http.timeout = timeout; + http.send(); + return http; + } + + function styles(node) { + return (node.nodeName === "LINK") && node.rel && node.rel.includes("stylesheet"); + } + + function scripts(node) { + return (node.nodeName === "SCRIPT") && node.hasAttribute("src"); + } + + function filter(url, http) { + let live = document; + let node = live.head.childNodes.length; + let next = (new DOMParser()).parseFromString(http.responseText, "text/html"); + + if (next.doctype === null || !http.getResponseHeader("content-type").includes("text/html")) return false; + + while (node--) { + if (styles(live.head.childNodes[node]) || scripts(live.head.childNodes[node])) { + // console.log("INFO: Persist:", live.head.childNodes[node]); + } else { + live.head.removeChild(live.head.childNodes[node]); + } + } + + while (next.head.firstChild) { + if (styles(next.head.firstChild) || scripts(next.head.firstChild)) { + next.head.removeChild(next.head.firstChild); + } else { + live.head.appendChild(next.head.firstChild); + } + } + + while (live.documentElement.attributes.length > 0) { + live.documentElement.removeAttribute( + live.documentElement.attributes[live.documentElement.attributes.length - 1].name, + ); + } + + while (next.documentElement.attributes.length > 0) { + live.documentElement.setAttribute( + next.documentElement.attributes[next.documentElement.attributes.length - 1].name, + next.documentElement.attributes[next.documentElement.attributes.length - 1].value, + ); + next.documentElement.removeAttribute( + next.documentElement.attributes[next.documentElement.attributes.length - 1].name, + ); + } + + live.body.parentElement.replaceChild(next.body, live.body); + } + + function persist() { + let persist = document.createElement("link"); + persist.rel = "location"; + persist.target = JSON.stringify(self.location); + document.head.appendChild(persist); + } + + self.addEventListener("DOMContentLoaded", function () { + if (localStorage[key] !== state) return; + persist(); + }); + + self.addEventListener("popstate", function (event) { + if (localStorage[key] !== state) return; + const link = JSON.parse(document.querySelector('link[rel="location"]').target); + const url = event.target.location; + const hashed = link.pathname === url.pathname; + if (hashed) return; + fetch(url, "GET", function (http) { + filter(url.href, http); + persist(); + self.document.dispatchEvent(new CustomEvent("URLChangedCustomEvent", { bubbles: true })); + }); + }); + + self.addEventListener("click", function (event) { + if (localStorage[key] !== state) return; + const links = document.querySelectorAll("a"); + for (let i = 0; i < links.length; i++) { + const active = links[i].contains(event.target); + const change = self.location.href !== links[i].href; + const view = links[i].attributes.hasOwnProperty("download") === false; + const local = self.location.origin === links[i].origin && links[i].target !== "_self"; + const hashed = self.location.pathname === links[i].pathname && links[i].href.includes("#"); + if (active && local && change && view && (hashed === false)) { + event.preventDefault(); + const url = links[i].href; + links[i].style.cursor = "wait"; + fetch(url, "GET", function (http) { + links[i].removeAttribute("style") + if (filter(url, http) === false) return self.location.href = url; + history.pushState({}, "", links[i].href); + persist(); + self.document.dispatchEvent(new CustomEvent("URLChangedCustomEvent", { bubbles: true })); + }); + } + } + }); +})(); + +/* + * Copyright (C) 2024 Thedro Neely + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ diff --git a/static/js/index.ts b/static/js/index.ts index 4b0951c..517445c 100644 --- a/static/js/index.ts +++ b/static/js/index.ts @@ -7,6 +7,7 @@ import "./fixedsearch.ts"; import "./autoplay.ts"; import "./hoverfix.ts"; import "./forms.ts"; +import "./domfilter.ts"; import "./timeago.ts"; console.log("INFO: Surface Control Complete"); -- cgit v1.2.3