feat(runtime): support viewport-ahead reveals

This commit is contained in:
slhx agent
2026-08-02 22:06:18 +02:00
parent c530dc5d8f
commit da28db9775
7 changed files with 62 additions and 19 deletions
+30 -12
View File
@@ -881,8 +881,14 @@
everyTimers.delete(el);
}
function revealedRootMargin(el) {
const ahead = Number(el.getAttribute("data-hemx-revealed-ahead") || "0");
const viewports = Number.isFinite(ahead) && ahead >= 0 ? ahead : 0;
return `0px 0px ${viewports * window.innerHeight}px 0px`;
}
function bindRevealed(root) {
let observer = revealObservers.get(root);
let observers = revealObservers.get(root);
forEachElement(root, (el) => {
if (!el.hasAttribute("data-hemx-revealed") || revealed.has(el)) return;
if (typeof IntersectionObserver === "undefined") {
@@ -890,6 +896,12 @@
schedule(el, "revealed");
return;
}
if (!observers) {
observers = new Map();
revealObservers.set(root, observers);
}
const rootMargin = revealedRootMargin(el);
let observer = observers.get(rootMargin);
if (!observer) {
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
@@ -898,8 +910,8 @@
observer.unobserve(entry.target);
schedule(entry.target, "revealed");
});
});
revealObservers.set(root, observer);
}, { rootMargin });
observers.set(rootMargin, observer);
}
observer.observe(el);
});
@@ -1123,25 +1135,30 @@
const source = sseSources.get(root);
if (source) source.close();
sseSources.delete(root);
const observer = revealObservers.get(root);
if (observer) observer.disconnect();
const observers = revealObservers.get(root);
if (observers) observers.forEach((observer) => observer.disconnect());
revealObservers.delete(root);
stopDescendantPolling(root);
}
function restoreRevealed(event) {
if (!event.persisted) return;
function rebindRevealed(resetDispatched) {
roots().forEach((root) => {
const observer = revealObservers.get(root);
if (observer) observer.disconnect();
const observers = revealObservers.get(root);
if (observers) observers.forEach((observer) => observer.disconnect());
revealObservers.delete(root);
forEachElement(root, (el) => {
if (attrEquals(el, "data-hemx-on", "revealed") || el.hasAttribute("data-hemx-revealed")) revealed.delete(el);
});
if (resetDispatched) {
forEachElement(root, (el) => {
if (attrEquals(el, "data-hemx-on", "revealed") || el.hasAttribute("data-hemx-revealed")) revealed.delete(el);
});
}
bindRevealed(root);
});
}
function restoreRevealed(event) {
if (event.persisted) rebindRevealed(true);
}
function start() {
roots().forEach((root) => {
root.setAttribute("data-hemx-request-timeout-ms", String(REQUEST_TIMEOUT_MS));
@@ -1189,6 +1206,7 @@
}
window.addEventListener("pageshow", restoreRevealed);
window.addEventListener("resize", () => rebindRevealed(false));
addEventListener("popstate", (event) => {
const root = roots()[0];
+3
View File
@@ -146,7 +146,10 @@ fn runtime_supports_tiny_delay_and_revealed_scheduling() {
assert!(source.contains("const revealObservers = new WeakMap()"));
assert!(source.contains("const revealed = new WeakSet()"));
assert!(source.contains("data-hemx-revealed"));
assert!(source.contains("data-hemx-revealed-ahead")); // req: convention/011 test
assert!(source.contains("typeof IntersectionObserver === \"undefined\""));
assert!(source.contains("rootMargin"));
assert!(source.contains("addEventListener(\"resize\", () => rebindRevealed(false))"));
assert!(source.contains("schedule(entry.target, \"revealed\")"));
assert!(source.contains("window.addEventListener(\"pageshow\", restoreRevealed)"));
assert!(source.contains("revealed.delete(el)")); // req: convention/014 test