release: WRNexusJS 0.8.0
This commit is contained in:
+189
-35
@@ -34,6 +34,138 @@ export const NAV_RUNTIME = String.raw`
|
||||
|
||||
var APP_ID = "app";
|
||||
var inFlight = null;
|
||||
var memory = Object.create(null);
|
||||
var keepAlive = Object.create(null);
|
||||
var keepAliveOrder = [];
|
||||
|
||||
function keepAliveKey(node) {
|
||||
return String(node && node.getAttribute("data-wrn-keepalive") || "");
|
||||
}
|
||||
|
||||
function retainKeepAlive(root) {
|
||||
Array.prototype.forEach.call(root.querySelectorAll("[data-wrn-keepalive]"), function (node) {
|
||||
var key = keepAliveKey(node);
|
||||
if (!key) return;
|
||||
if (!keepAlive[key]) keepAliveOrder.push(key);
|
||||
keepAlive[key] = node;
|
||||
if (node.parentNode) node.parentNode.removeChild(node);
|
||||
});
|
||||
while (keepAliveOrder.length > 32) {
|
||||
var expired = keepAliveOrder.shift();
|
||||
if (expired && keepAlive[expired]) {
|
||||
dispose(keepAlive[expired]);
|
||||
delete keepAlive[expired];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function restoreKeepAlive(root) {
|
||||
var placeholders = [];
|
||||
if (root.matches && root.matches("[data-wrn-keepalive]")) placeholders.push(root);
|
||||
Array.prototype.forEach.call(root.querySelectorAll("[data-wrn-keepalive]"), function (node) { placeholders.push(node); });
|
||||
placeholders.forEach(function (placeholder) {
|
||||
var saved = keepAlive[keepAliveKey(placeholder)];
|
||||
if (saved && placeholder.parentNode) placeholder.parentNode.replaceChild(saved, placeholder);
|
||||
});
|
||||
}
|
||||
|
||||
function preservationPolicy(doc) {
|
||||
var meta = doc.querySelector('meta[name="wrnexus-preserve"]');
|
||||
var out = Object.create(null);
|
||||
String(meta && meta.getAttribute("content") || "").split(",").forEach(function (name) {
|
||||
if (name) out[name] = true;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function syncPreservationPolicy(nextDocument) {
|
||||
var selector = 'meta[name="wrnexus-preserve"]';
|
||||
var current = document.querySelector(selector);
|
||||
var next = nextDocument.querySelector(selector);
|
||||
if (!next) {
|
||||
if (current) current.remove();
|
||||
return;
|
||||
}
|
||||
if (!current) {
|
||||
current = document.createElement("meta");
|
||||
current.setAttribute("name", "wrnexus-preserve");
|
||||
document.head.appendChild(current);
|
||||
}
|
||||
current.setAttribute("content", next.getAttribute("content") || "");
|
||||
}
|
||||
|
||||
function stateKey(url) {
|
||||
try {
|
||||
var parsed = new URL(url, location.href);
|
||||
return "wrnexus:navigation:" + parsed.pathname + parsed.search;
|
||||
} catch (_) {
|
||||
return "wrnexus:navigation:" + String(url);
|
||||
}
|
||||
}
|
||||
|
||||
function safeField(field) {
|
||||
var type = String(field.type || "").toLowerCase();
|
||||
var name = String(field.name || field.id || "").toLowerCase();
|
||||
return type !== "password" && type !== "file" && type !== "hidden" &&
|
||||
!field.hasAttribute("data-no-preserve") &&
|
||||
!/(?:csrf|token|secret|password|credential)/.test(name);
|
||||
}
|
||||
|
||||
function capturePage(url) {
|
||||
var policy = preservationPolicy(document);
|
||||
var state = { fields: Object.create(null), elements: Object.create(null) };
|
||||
if (policy.scroll) state.scroll = [window.scrollX || 0, window.scrollY || 0];
|
||||
if (policy.forms || policy.filters || policy.pagination || policy.workflow) {
|
||||
Array.prototype.forEach.call(document.querySelectorAll("#app input,#app select,#app textarea"), function (field, index) {
|
||||
if (!safeField(field)) return;
|
||||
var key = field.name || field.id || String(index);
|
||||
state.fields[key] = { value: field.value, checked: !!field.checked, selectedIndex: field.selectedIndex };
|
||||
});
|
||||
}
|
||||
if (policy.tabs || policy.expanded || policy.component) {
|
||||
Array.prototype.forEach.call(document.querySelectorAll("#app [data-wrn-preserve]"), function (node, index) {
|
||||
var key = node.getAttribute("data-wrn-preserve") || node.id || String(index);
|
||||
state.elements[key] = {
|
||||
selected: node.getAttribute("aria-selected"),
|
||||
expanded: node.getAttribute("aria-expanded"),
|
||||
value: "value" in node ? node.value : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
memory[stateKey(url)] = state;
|
||||
try { sessionStorage.setItem(stateKey(url), JSON.stringify(state)); } catch (_) {}
|
||||
}
|
||||
|
||||
function restorePage(url, isPop) {
|
||||
var policy = preservationPolicy(document);
|
||||
var state = memory[stateKey(url)];
|
||||
if (!state) {
|
||||
try { state = JSON.parse(sessionStorage.getItem(stateKey(url)) || "null"); } catch (_) {}
|
||||
}
|
||||
if (state && (policy.forms || policy.filters || policy.pagination || policy.workflow)) {
|
||||
Array.prototype.forEach.call(document.querySelectorAll("#app input,#app select,#app textarea"), function (field, index) {
|
||||
if (!safeField(field)) return;
|
||||
var saved = state.fields && state.fields[field.name || field.id || String(index)];
|
||||
if (!saved) return;
|
||||
if (field.type === "checkbox" || field.type === "radio") field.checked = !!saved.checked;
|
||||
else field.value = saved.value;
|
||||
if (field.tagName === "SELECT") field.selectedIndex = saved.selectedIndex;
|
||||
field.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
}
|
||||
if (state && (policy.tabs || policy.expanded || policy.component)) {
|
||||
Array.prototype.forEach.call(document.querySelectorAll("#app [data-wrn-preserve]"), function (node, index) {
|
||||
var key = node.getAttribute("data-wrn-preserve") || node.id || String(index);
|
||||
var saved = state.elements && state.elements[key];
|
||||
if (!saved) return;
|
||||
if (saved.selected != null) node.setAttribute("aria-selected", saved.selected);
|
||||
if (saved.expanded != null) node.setAttribute("aria-expanded", saved.expanded);
|
||||
if (saved.value != null && "value" in node) node.value = saved.value;
|
||||
});
|
||||
}
|
||||
if (state && policy.scroll && state.scroll) window.scrollTo(state.scroll[0], state.scroll[1]);
|
||||
else if (!isPop) window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function pathOf(src) {
|
||||
return String(src).split("?")[0];
|
||||
@@ -365,6 +497,8 @@ export const NAV_RUNTIME = String.raw`
|
||||
return;
|
||||
}
|
||||
|
||||
capturePage(location.href);
|
||||
|
||||
var incomingApp =
|
||||
doc.getElementById(APP_ID);
|
||||
|
||||
@@ -380,6 +514,8 @@ export const NAV_RUNTIME = String.raw`
|
||||
document.title = doc.title;
|
||||
}
|
||||
|
||||
syncPreservationPolicy(doc);
|
||||
|
||||
syncWrnStyles(doc);
|
||||
|
||||
var importedNodes = [];
|
||||
@@ -402,6 +538,7 @@ export const NAV_RUNTIME = String.raw`
|
||||
* connected. This is important for components that remove window or
|
||||
* document listeners during lifecycle.unmount.
|
||||
*/
|
||||
retainKeepAlive(currentApp);
|
||||
dispose(currentApp);
|
||||
|
||||
try {
|
||||
@@ -413,6 +550,7 @@ export const NAV_RUNTIME = String.raw`
|
||||
currentApp,
|
||||
importedNodes,
|
||||
);
|
||||
restoreKeepAlive(currentApp);
|
||||
} catch (_) {
|
||||
hardNavigate(url);
|
||||
return;
|
||||
@@ -439,9 +577,10 @@ export const NAV_RUNTIME = String.raw`
|
||||
url,
|
||||
);
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
restorePage(url, isPop);
|
||||
|
||||
dispatchNavigationEvent(url);
|
||||
}
|
||||
|
||||
@@ -449,50 +588,65 @@ export const NAV_RUNTIME = String.raw`
|
||||
var token = {};
|
||||
|
||||
inFlight = token;
|
||||
window.dispatchEvent(new CustomEvent("wrnexus:navigation-start", { detail: { url: url } }));
|
||||
|
||||
fetch(url, {
|
||||
var pending = prefetched.get(url) || requestDocument(url, false);
|
||||
prefetched.delete(url);
|
||||
pending
|
||||
.then(function (result) {
|
||||
if (inFlight !== token) return null;
|
||||
if (result.redirectedUrl) url = result.redirectedUrl;
|
||||
if (result.contentType.indexOf("text/html") === -1) {
|
||||
hardNavigate(url);
|
||||
return null;
|
||||
}
|
||||
render(result.text, url, isPop);
|
||||
return null;
|
||||
})
|
||||
.catch(function () {
|
||||
if (inFlight === token) hardNavigate(url);
|
||||
});
|
||||
}
|
||||
|
||||
var prefetched = new Map();
|
||||
function requestDocument(url, isPrefetch) {
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
"x-wrnexus-nav": "1",
|
||||
...(isPrefetch ? { "x-wrnexus-prefetch": "1" } : {}),
|
||||
accept: "text/html",
|
||||
},
|
||||
|
||||
credentials: "same-origin",
|
||||
})
|
||||
.then(function (response) {
|
||||
if (inFlight !== token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.redirected && response.url) {
|
||||
url = response.url;
|
||||
}
|
||||
|
||||
var contentType =
|
||||
response.headers.get("content-type") ||
|
||||
"";
|
||||
|
||||
if (
|
||||
contentType.indexOf("text/html") === -1
|
||||
) {
|
||||
hardNavigate(url);
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.text().then(function (text) {
|
||||
if (inFlight !== token) {
|
||||
return;
|
||||
}
|
||||
|
||||
render(text, url, isPop);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
if (inFlight === token) {
|
||||
hardNavigate(url);
|
||||
}
|
||||
}).then(function (response) {
|
||||
var contentType = response.headers.get("content-type") || "";
|
||||
return response.text().then(function (text) {
|
||||
return {
|
||||
text: text,
|
||||
contentType: contentType,
|
||||
redirectedUrl: response.redirected && response.url ? response.url : "",
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function prefetch(anchor) {
|
||||
if (!isLocalLink(anchor) || anchor.hasAttribute("data-no-prefetch")) return;
|
||||
var url = anchor.href;
|
||||
if (url === location.href || prefetched.has(url)) return;
|
||||
prefetched.set(url, requestDocument(url, true));
|
||||
while (prefetched.size > 20) prefetched.delete(prefetched.keys().next().value);
|
||||
}
|
||||
|
||||
document.addEventListener("pointerover", function (event) {
|
||||
var anchor = event.target && event.target.closest ? event.target.closest("a") : null;
|
||||
prefetch(anchor);
|
||||
}, { passive: true });
|
||||
document.addEventListener("focusin", function (event) {
|
||||
var anchor = event.target && event.target.closest ? event.target.closest("a") : null;
|
||||
prefetch(anchor);
|
||||
});
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
function (event) {
|
||||
|
||||
Reference in New Issue
Block a user