feat(runtime): add tiny trigger timing conventions
Add boring runtime-level trigger timing primitives for delay, revealed, and interval while keeping them as attribute conventions rather than core effects or plugin surfaces. Promote lazy-load, infinite-scroll, progress-bar, value-select, and reset-user-input in examples/html_examples to runnable hemx patterns with exact htmx slugs and generated resources. req: convention/001 req: convention/003 req: convention/005 req: htmx_equivalents/001 req: htmx_equivalents/002 req: examples/001
This commit is contained in:
+34
-3
@@ -9,6 +9,8 @@
|
||||
const queues = new WeakMap();
|
||||
const timers = new WeakMap();
|
||||
const everyTimers = new WeakMap();
|
||||
const revealObservers = new WeakMap();
|
||||
const revealed = new WeakSet();
|
||||
const pendingClassStates = new WeakMap();
|
||||
const indicatorStates = new WeakMap();
|
||||
const busyStates = new WeakMap();
|
||||
@@ -654,14 +656,18 @@
|
||||
});
|
||||
});
|
||||
bindPolling(root);
|
||||
bindRevealed(root);
|
||||
}
|
||||
|
||||
function schedule(el, eventName, source = el) {
|
||||
const debounce = duration(el.getAttribute("data-hemx-debounce"));
|
||||
const delay = duration(el.getAttribute("data-hemx-delay"));
|
||||
const throttle = duration(el.getAttribute("data-hemx-throttle"));
|
||||
if (debounce) {
|
||||
clearTimeout(timers.get(el));
|
||||
timers.set(el, setTimeout(() => send(el, eventName, source), debounce));
|
||||
} else if (delay) {
|
||||
setTimeout(() => send(el, eventName, source), delay);
|
||||
} else if (throttle) {
|
||||
if (timers.get(el)) return;
|
||||
send(el, eventName, source).finally(() => setTimeout(() => timers.delete(el), throttle));
|
||||
@@ -672,10 +678,11 @@
|
||||
|
||||
function bindPolling(root) {
|
||||
forEachElement(root, (el) => {
|
||||
if (!el.hasAttribute("data-hemx-every") || everyTimers.has(el)) return;
|
||||
const ms = duration(el.getAttribute("data-hemx-every"));
|
||||
if ((!el.hasAttribute("data-hemx-every") && !el.hasAttribute("data-hemx-interval")) || everyTimers.has(el)) return;
|
||||
const eventName = el.hasAttribute("data-hemx-interval") ? "interval" : "every";
|
||||
const ms = duration(el.getAttribute("data-hemx-interval") || el.getAttribute("data-hemx-every"));
|
||||
if (!ms) return;
|
||||
everyTimers.set(el, setInterval(() => document.contains(el) ? send(el, "every") : stopPolling(el), ms));
|
||||
everyTimers.set(el, setInterval(() => document.contains(el) ? send(el, eventName) : stopPolling(el), ms));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -684,6 +691,30 @@
|
||||
everyTimers.delete(el);
|
||||
}
|
||||
|
||||
function bindRevealed(root) {
|
||||
let observer = revealObservers.get(root);
|
||||
forEachElement(root, (el) => {
|
||||
if (!el.hasAttribute("data-hemx-revealed") || revealed.has(el)) return;
|
||||
if (typeof IntersectionObserver === "undefined") {
|
||||
revealed.add(el);
|
||||
schedule(el, "revealed");
|
||||
return;
|
||||
}
|
||||
if (!observer) {
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting || revealed.has(entry.target)) return;
|
||||
revealed.add(entry.target);
|
||||
observer.unobserve(entry.target);
|
||||
schedule(entry.target, "revealed");
|
||||
});
|
||||
});
|
||||
revealObservers.set(root, observer);
|
||||
}
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
function bindSse(root) {
|
||||
const url = root.getAttribute("data-hemx-sse");
|
||||
if (!url || sseSources.has(root) || typeof EventSource === "undefined") return;
|
||||
|
||||
@@ -101,19 +101,32 @@ fn runtime_interval_dispatch_avoids_duplicate_timers() {
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("const everyTimers = new WeakMap()"));
|
||||
assert!(source.contains("forEachElement(root, (el) =>"));
|
||||
assert!(source.contains("!el.hasAttribute(\"data-hemx-every\") || everyTimers.has(el)"));
|
||||
assert!(
|
||||
source.contains("if (!el.hasAttribute(\"data-hemx-every\") || everyTimers.has(el)) return")
|
||||
);
|
||||
assert!(source.contains("data-hemx-interval"));
|
||||
assert!(source.contains(
|
||||
"setInterval(() => document.contains(el) ? send(el, \"every\") : stopPolling(el), ms)"
|
||||
"eventName = el.hasAttribute(\"data-hemx-interval\") ? \"interval\" : \"every\""
|
||||
));
|
||||
assert!(source.contains(
|
||||
"setInterval(() => document.contains(el) ? send(el, eventName) : stopPolling(el), ms)"
|
||||
));
|
||||
assert!(source.contains("function stopPolling(el)"));
|
||||
assert!(source.contains("clearInterval(everyTimers.get(el))"));
|
||||
assert!(source.contains("everyTimers.delete(el)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_supports_tiny_delay_and_revealed_scheduling() {
|
||||
// req: convention/001 req: convention/003 req: convention/005
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("data-hemx-delay"));
|
||||
assert!(source.contains("setTimeout(() => send(el, eventName, source), delay)"));
|
||||
assert!(source.contains("const revealObservers = new WeakMap()"));
|
||||
assert!(source.contains("const revealed = new WeakSet()"));
|
||||
assert!(source.contains("data-hemx-revealed"));
|
||||
assert!(source.contains("typeof IntersectionObserver === \"undefined\""));
|
||||
assert!(source.contains("schedule(entry.target, \"revealed\")"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_toggles_pending_conventions_around_requests() {
|
||||
// req: convention/007 req: convention/008
|
||||
|
||||
Reference in New Issue
Block a user