fix(js): remove runtime selector traversal
Replace runtime querySelector/closest/matches traversal with explicit element walks and generated-id predicates, preserving existing runtime convention coverage. req: runtime/001 req: target_policy/001 req: target_policy/002
This commit is contained in:
+119
-35
@@ -13,28 +13,34 @@
|
|||||||
const dragKeys = new WeakMap();
|
const dragKeys = new WeakMap();
|
||||||
|
|
||||||
function roots() {
|
function roots() {
|
||||||
return Array.from(document.querySelectorAll(`[${ROOT}]`));
|
const found = [];
|
||||||
|
forEachElement(document, (el) => { if (el.hasAttribute(ROOT)) found.push(el); });
|
||||||
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rootOf(node) {
|
function rootOf(node) {
|
||||||
return node && node.closest ? node.closest(`[${ROOT}]`) : null;
|
for (let el = node; el && el !== document; el = el.parentElement) {
|
||||||
|
if (el.hasAttribute && el.hasAttribute(ROOT)) return el;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formOwner(el) {
|
function formOwner(el) {
|
||||||
if (!el || el.tagName === "FORM") return el;
|
if (!el || el.tagName === "FORM") return el;
|
||||||
if (el.getAttribute && el.getAttribute("form")) return document.getElementById(el.getAttribute("form"));
|
if (el.getAttribute && el.getAttribute("form")) return document.getElementById(el.getAttribute("form"));
|
||||||
return el.closest ? el.closest("form") : null;
|
return closestInRoot(el, rootOf(el) || document, (node) => node.tagName === "FORM");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formHandleId(form) {
|
function formHandleId(form) {
|
||||||
if (!form) return null;
|
if (!form) return null;
|
||||||
const raw = form.getAttribute(HID) || (form.querySelector(`[${HID}]`) && form.querySelector(`[${HID}]`).getAttribute(HID));
|
const holder = form.hasAttribute(HID) ? form : firstElement(form, (el) => el.hasAttribute(HID));
|
||||||
|
const raw = holder && holder.getAttribute(HID);
|
||||||
return raw && /^\d+$/.test(raw) ? raw : null;
|
return raw && /^\d+$/.test(raw) ? raw : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closestInRoot(start, root, selector) {
|
function closestInRoot(start, root, predicate) {
|
||||||
for (let node = start; node && node !== root.parentNode; node = node.parentNode) {
|
for (let node = start; node && node !== root.parentNode; node = node.parentElement) {
|
||||||
if (node.matches && node.matches(selector)) return node;
|
if (predicate(node)) return node;
|
||||||
if (node === root) break;
|
if (node === root) break;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -61,9 +67,11 @@
|
|||||||
const klass = el.getAttribute("data-slhx-pending-class");
|
const klass = el.getAttribute("data-slhx-pending-class");
|
||||||
if (klass) el.classList.toggle(klass, on);
|
if (klass) el.classList.toggle(klass, on);
|
||||||
const root = rootOf(el) || document;
|
const root = rootOf(el) || document;
|
||||||
root.querySelectorAll("[data-slhx-indicator]").forEach((i) => { i.hidden = !on; });
|
forEachElement(root, (i) => { if (i.hasAttribute("data-slhx-indicator")) i.hidden = !on; });
|
||||||
if (el.hasAttribute("data-slhx-disable-while-pending")) {
|
if (el.hasAttribute("data-slhx-disable-while-pending")) {
|
||||||
const controls = el.matches("button,input,select,textarea") ? [el] : el.querySelectorAll("button,input,select,textarea");
|
const controls = [];
|
||||||
|
if (isDisableControl(el)) controls.push(el);
|
||||||
|
forEachElement(el, (child) => { if (isDisableControl(child)) controls.push(child); });
|
||||||
controls.forEach((c) => { c.disabled = on; });
|
controls.forEach((c) => { c.disabled = on; });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,7 +287,59 @@
|
|||||||
if (isAtom(ref)) return null;
|
if (isAtom(ref)) return null;
|
||||||
if (ref.scope && ref.scope.kind === "key") return keyedTarget(scope, ref.resource.id, ref.scope.value);
|
if (ref.scope && ref.scope.kind === "key") return keyedTarget(scope, ref.resource.id, ref.scope.value);
|
||||||
if (ref.scope && ref.scope.kind === "field") return fieldTarget(scope, ref.resource.id, ref.scope.value);
|
if (ref.scope && ref.scope.kind === "field") return fieldTarget(scope, ref.resource.id, ref.scope.value);
|
||||||
return scope.querySelector(`[data-sid="${ref.resource.id}"], [data-slot-id="${ref.resource.id}"]`);
|
return generatedTarget(scope, ref.resource.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstElement(scope, predicate) {
|
||||||
|
const stack = [];
|
||||||
|
for (let node = scope && scope.firstElementChild; node; node = node.nextElementSibling) stack.push(node);
|
||||||
|
while (stack.length) {
|
||||||
|
const node = stack.shift();
|
||||||
|
if (predicate(node)) return node;
|
||||||
|
for (let child = node.firstElementChild; child; child = child.nextElementSibling) stack.push(child);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attrEquals(el, name, value) {
|
||||||
|
return el && el.getAttribute && el.getAttribute(name) === String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatedResource(el, id) {
|
||||||
|
return attrEquals(el, "data-sid", id) || attrEquals(el, "data-slot-id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatedForm(el, id) {
|
||||||
|
return attrEquals(el, "data-fid", id) || attrEquals(el, "data-form-id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatedTarget(scope, id) {
|
||||||
|
if (scope && generatedResource(scope, id)) return scope;
|
||||||
|
return firstElement(scope, (el) => generatedResource(el, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function withinGeneratedResource(el, scope, id) {
|
||||||
|
for (let node = el; node && node !== scope.parentNode; node = node.parentElement) {
|
||||||
|
if (generatedResource(node, id)) return true;
|
||||||
|
if (node === scope) break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withinGeneratedForm(el, scope, id) {
|
||||||
|
for (let node = el; node && node !== scope.parentNode; node = node.parentElement) {
|
||||||
|
if (generatedForm(node, id)) return true;
|
||||||
|
if (node === scope) break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInputControl(el) {
|
||||||
|
return el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDisableControl(el) {
|
||||||
|
return isInputControl(el) || (el && el.tagName === "BUTTON");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAtom(ref) {
|
function isAtom(ref) {
|
||||||
@@ -301,29 +361,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function keyedTarget(scope, id, key) {
|
function keyedTarget(scope, id, key) {
|
||||||
const escapedKey = cssEscape(key);
|
return firstElement(scope, (el) => attrEquals(el, "data-key", key) && withinGeneratedResource(el, scope, id));
|
||||||
return scope.querySelector(`[data-sid="${id}"][data-key="${escapedKey}"], [data-sid="${id}"] [data-key="${escapedKey}"], [data-slot-id="${id}"][data-key="${escapedKey}"], [data-slot-id="${id}"] [data-key="${escapedKey}"]`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fieldTarget(scope, id, field) {
|
function fieldTarget(scope, id, field) {
|
||||||
const escapedField = cssEscape(field);
|
return firstElement(scope, (el) => attrEquals(el, "name", field) && withinGeneratedForm(el, scope, id));
|
||||||
return scope.querySelector(`[data-fid="${id}"] [name="${escapedField}"], [data-form-id="${id}"] [name="${escapedField}"]`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formErrorTarget(scope, id, field) {
|
function formErrorTarget(scope, id, field) {
|
||||||
const escapedField = cssEscape(field);
|
return firstElement(scope, (el) => attrEquals(el, "data-slhx-error-for", field) && withinGeneratedForm(el, scope, id)) ||
|
||||||
return scope.querySelector(`[data-fid="${id}"] [data-slhx-error-for="${escapedField}"], [data-form-id="${id}"] [data-slhx-error-for="${escapedField}"]`) ||
|
firstElement(scope, (el) => attrEquals(el, "name", field) && withinGeneratedForm(el, scope, id));
|
||||||
scope.querySelector(`[data-fid="${id}"] [name="${escapedField}"], [data-form-id="${id}"] [name="${escapedField}"]`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function putFormError(target, message) {
|
function putFormError(target, message) {
|
||||||
if (target.matches && target.matches("input,textarea,select")) target.setCustomValidity(message);
|
if (isInputControl(target)) target.setCustomValidity(message);
|
||||||
else target.textContent = message;
|
else target.textContent = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
function putPayload(target, payload) {
|
function putPayload(target, payload) {
|
||||||
if (payload.kind === "html") target.innerHTML = payload.value;
|
if (payload.kind === "html") target.innerHTML = payload.value;
|
||||||
else if (target.matches && target.matches("input,textarea,select")) target.value = payload.value;
|
else if (isInputControl(target)) target.value = payload.value;
|
||||||
else target.textContent = payload.value;
|
else target.textContent = payload.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,14 +396,14 @@
|
|||||||
|
|
||||||
function handleRuntimeEvent(scope, name, payload) {
|
function handleRuntimeEvent(scope, name, payload) {
|
||||||
if (name === "slhx:form-reset") {
|
if (name === "slhx:form-reset") {
|
||||||
const form = scope.querySelector(`[data-fid="${payload}"], [data-form-id="${payload}"]`);
|
const form = generatedFormTarget(scope, payload);
|
||||||
if (form && form.reset) form.reset();
|
if (form && form.reset) form.reset();
|
||||||
} else if (name === "slhx:form-error") {
|
} else if (name === "slhx:form-error") {
|
||||||
const [id, field, message] = String(payload).split("\u001f");
|
const [id, field, message] = String(payload).split("\u001f");
|
||||||
const target = formErrorTarget(scope, id, field);
|
const target = formErrorTarget(scope, id, field);
|
||||||
if (target) putFormError(target, message || "");
|
if (target) putFormError(target, message || "");
|
||||||
} else if (name === "slhx:form-disable-while-pending") {
|
} else if (name === "slhx:form-disable-while-pending") {
|
||||||
const form = scope.querySelector(`[data-fid="${payload}"], [data-form-id="${payload}"]`);
|
const form = generatedFormTarget(scope, payload);
|
||||||
if (form) form.setAttribute("data-slhx-disable-while-pending", "");
|
if (form) form.setAttribute("data-slhx-disable-while-pending", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,7 +420,7 @@
|
|||||||
function applyHtml(html, root, title) {
|
function applyHtml(html, root, title) {
|
||||||
const scope = root || document;
|
const scope = root || document;
|
||||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||||
const template = doc.querySelector("template[data-slhx]");
|
const template = firstElement(doc, (el) => el.tagName === "TEMPLATE" && el.hasAttribute("data-slhx"));
|
||||||
const lowered = replaceLoweredSlots(scope, doc);
|
const lowered = replaceLoweredSlots(scope, doc);
|
||||||
const named = replaceSlot(scope, doc, "content", lowered ? undefined : (template ? template.innerHTML : html));
|
const named = replaceSlot(scope, doc, "content", lowered ? undefined : (template ? template.innerHTML : html));
|
||||||
if (!lowered && !named) {
|
if (!lowered && !named) {
|
||||||
@@ -371,16 +428,18 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
replaceSlot(scope, doc, "nav");
|
replaceSlot(scope, doc, "nav");
|
||||||
const nextTitle = title || (doc.querySelector("title") && doc.querySelector("title").textContent);
|
const titleEl = firstElement(doc, (el) => el.tagName === "TITLE");
|
||||||
|
const nextTitle = title || (titleEl && titleEl.textContent);
|
||||||
if (nextTitle) document.title = nextTitle;
|
if (nextTitle) document.title = nextTitle;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceLoweredSlots(scope, doc) {
|
function replaceLoweredSlots(scope, doc) {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
doc.querySelectorAll("[data-sid], [data-slot-id]").forEach((source) => {
|
forEachElement(doc.body || doc, (source) => {
|
||||||
const id = source.getAttribute("data-sid") || source.getAttribute("data-slot-id");
|
const id = source.getAttribute("data-sid") || source.getAttribute("data-slot-id");
|
||||||
const target = scope.querySelector(`[data-sid="${cssEscape(id)}"], [data-slot-id="${cssEscape(id)}"]`);
|
if (!id) return;
|
||||||
|
const target = generatedTarget(scope, id);
|
||||||
if (target) {
|
if (target) {
|
||||||
target.innerHTML = source.innerHTML;
|
target.innerHTML = source.innerHTML;
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -389,10 +448,26 @@
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function forEachElement(scope, visit) {
|
||||||
|
for (let node = scope && scope.firstElementChild; node; node = node.nextElementSibling) {
|
||||||
|
visit(node);
|
||||||
|
forEachElement(node, visit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatedFormTarget(scope, id) {
|
||||||
|
if (scope && generatedForm(scope, id)) return scope;
|
||||||
|
return firstElement(scope, (el) => generatedForm(el, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function namedSlot(el, name) {
|
||||||
|
return attrEquals(el, "data-slhx-slot", name) || attrEquals(el, "data-slot", name);
|
||||||
|
}
|
||||||
|
|
||||||
function replaceSlot(scope, doc, name, fallback) {
|
function replaceSlot(scope, doc, name, fallback) {
|
||||||
const target = scope.querySelector(`[data-slhx-slot="${name}"], [data-slot="${name}"]`);
|
const target = firstElement(scope, (el) => namedSlot(el, name));
|
||||||
if (!target) return false;
|
if (!target) return false;
|
||||||
const source = doc.querySelector(`[data-slhx-slot="${name}"], [data-slot="${name}"]`);
|
const source = firstElement(doc, (el) => namedSlot(el, name));
|
||||||
if (!source && fallback === undefined) return false;
|
if (!source && fallback === undefined) return false;
|
||||||
target.innerHTML = source ? source.innerHTML : fallback;
|
target.innerHTML = source ? source.innerHTML : fallback;
|
||||||
return true;
|
return true;
|
||||||
@@ -412,7 +487,7 @@
|
|||||||
["click", "submit", "input", "change", "dragstart", "dragover", "drop"].forEach((name) => {
|
["click", "submit", "input", "change", "dragstart", "dragover", "drop"].forEach((name) => {
|
||||||
root.addEventListener(name, (event) => {
|
root.addEventListener(name, (event) => {
|
||||||
if (name === "dragstart") {
|
if (name === "dragstart") {
|
||||||
const item = closestInRoot(event.target, root, "[data-key]");
|
const item = closestInRoot(event.target, root, (el) => el.hasAttribute("data-key"));
|
||||||
if (item) {
|
if (item) {
|
||||||
dragKeys.set(root, item.getAttribute("data-key"));
|
dragKeys.set(root, item.getAttribute("data-key"));
|
||||||
if (event.dataTransfer) event.dataTransfer.setData("text/plain", item.getAttribute("data-key"));
|
if (event.dataTransfer) event.dataTransfer.setData("text/plain", item.getAttribute("data-key"));
|
||||||
@@ -420,18 +495,21 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === "dragover") {
|
if (name === "dragover") {
|
||||||
const drop = closestInRoot(event.target, root, `[${HID}][data-slhx-on="drop"]`);
|
const drop = closestInRoot(event.target, root, (el) => el.hasAttribute(HID) && el.getAttribute("data-slhx-on") === "drop");
|
||||||
if (drop) event.preventDefault();
|
if (drop) event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nav = closestInRoot(event.target, root, "a[data-slhx-nav], [data-slhx-boost] a[href]");
|
const nav = closestInRoot(event.target, root, (el) =>
|
||||||
|
(el.tagName === "A" && el.hasAttribute("data-slhx-nav")) ||
|
||||||
|
(el.tagName === "A" && el.hasAttribute("href") && closestInRoot(el.parentElement, root, (node) => node.hasAttribute("data-slhx-boost")))
|
||||||
|
);
|
||||||
if (name === "click" && nav && sameOriginNav(event, nav)) {
|
if (name === "click" && nav && sameOriginNav(event, nav)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
navigate(nav);
|
navigate(nav);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === "click") {
|
if (name === "click") {
|
||||||
const direct = closestInRoot(event.target, root, `[${HID}]`);
|
const direct = closestInRoot(event.target, root, (el) => el.hasAttribute(HID));
|
||||||
if (direct && defaultEvent(direct) === "click") {
|
if (direct && defaultEvent(direct) === "click") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
schedule(direct, name);
|
schedule(direct, name);
|
||||||
@@ -439,7 +517,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (name === "click") {
|
if (name === "click") {
|
||||||
const submitter = closestInRoot(event.target, root, "button[type=submit], input[type=submit], button:not([type])");
|
const submitter = closestInRoot(event.target, root, (el) =>
|
||||||
|
(el.tagName === "BUTTON" && (!el.hasAttribute("type") || el.getAttribute("type") === "submit")) ||
|
||||||
|
(el.tagName === "INPUT" && el.getAttribute("type") === "submit")
|
||||||
|
);
|
||||||
const form = formOwner(submitter);
|
const form = formOwner(submitter);
|
||||||
if (form && root.contains(form) && formHandleId(form)) {
|
if (form && root.contains(form) && formHandleId(form)) {
|
||||||
if (form.reportValidity && !form.reportValidity()) return;
|
if (form.reportValidity && !form.reportValidity()) return;
|
||||||
@@ -448,7 +529,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let el = closestInRoot(event.target, root, `[${HID}], [data-slhx-boost] form`);
|
let el = closestInRoot(event.target, root, (node) =>
|
||||||
|
node.hasAttribute(HID) ||
|
||||||
|
(node.tagName === "FORM" && closestInRoot(node.parentElement, root, (parent) => parent.hasAttribute("data-slhx-boost")))
|
||||||
|
);
|
||||||
if (name === "submit" && !el && event.target && event.target.tagName === "FORM" && formHandleId(event.target)) el = event.target;
|
if (name === "submit" && !el && event.target && event.target.tagName === "FORM" && formHandleId(event.target)) el = event.target;
|
||||||
if (!el || defaultEvent(el) !== name) return;
|
if (!el || defaultEvent(el) !== name) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -473,8 +557,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindPolling(root) {
|
function bindPolling(root) {
|
||||||
root.querySelectorAll("[data-slhx-every]").forEach((el) => {
|
forEachElement(root, (el) => {
|
||||||
if (timers.has(el)) return;
|
if (!el.hasAttribute("data-slhx-every") || timers.has(el)) return;
|
||||||
const ms = duration(el.getAttribute("data-slhx-every"));
|
const ms = duration(el.getAttribute("data-slhx-every"));
|
||||||
if (!ms) return;
|
if (!ms) return;
|
||||||
timers.set(el, setInterval(() => document.contains(el) ? send(el, "every") : clearInterval(timers.get(el)), ms));
|
timers.set(el, setInterval(() => document.contains(el) ? send(el, "every") : clearInterval(timers.get(el)), ms));
|
||||||
|
|||||||
+18
-11
@@ -26,9 +26,11 @@ fn runtime_targets_generated_resources_not_response_selectors() {
|
|||||||
let source = slhx_js::RUNTIME_JS;
|
let source = slhx_js::RUNTIME_JS;
|
||||||
|
|
||||||
assert!(source.contains("function targetFor(scope, ref)"));
|
assert!(source.contains("function targetFor(scope, ref)"));
|
||||||
assert!(source.contains("return scope.querySelector(`[data-sid=\"${ref.resource.id}\"], [data-slot-id=\"${ref.resource.id}\"]`)"));
|
assert!(source.contains("return generatedTarget(scope, ref.resource.id)"));
|
||||||
assert!(source.contains("return scope.querySelector(`[data-sid=\"${id}\"][data-key=\"${escapedKey}\"], [data-sid=\"${id}\"] [data-key=\"${escapedKey}\"], [data-slot-id=\"${id}\"][data-key=\"${escapedKey}\"], [data-slot-id=\"${id}\"] [data-key=\"${escapedKey}\"]`)"));
|
assert!(source.contains("function firstElement(scope, predicate)"));
|
||||||
assert!(source.contains("doc.querySelectorAll(\"[data-sid], [data-slot-id]\")"));
|
assert!(source.contains("function generatedResource(el, id)"));
|
||||||
|
assert!(source.contains("return firstElement(scope, (el) => attrEquals(el, \"data-key\", key) && withinGeneratedResource(el, scope, id))"));
|
||||||
|
assert!(source.contains("const target = generatedTarget(scope, id)"));
|
||||||
assert!(source.contains("if (!target) return missing(scope, op.target)"));
|
assert!(source.contains("if (!target) return missing(scope, op.target)"));
|
||||||
assert!(!source.contains("data-slhx-target"));
|
assert!(!source.contains("data-slhx-target"));
|
||||||
assert!(!source.contains("data-slhx-select"));
|
assert!(!source.contains("data-slhx-select"));
|
||||||
@@ -39,8 +41,9 @@ fn runtime_interval_dispatch_avoids_duplicate_timers() {
|
|||||||
// req: convention/005
|
// req: convention/005
|
||||||
let source = slhx_js::RUNTIME_JS;
|
let source = slhx_js::RUNTIME_JS;
|
||||||
|
|
||||||
assert!(source.contains("root.querySelectorAll(\"[data-slhx-every]\")"));
|
assert!(source.contains("forEachElement(root, (el) =>"));
|
||||||
assert!(source.contains("if (timers.has(el)) return"));
|
assert!(source.contains("!el.hasAttribute(\"data-slhx-every\") || timers.has(el)"));
|
||||||
|
assert!(source.contains("if (!el.hasAttribute(\"data-slhx-every\") || timers.has(el)) return"));
|
||||||
assert!(source.contains("setInterval(() => document.contains(el) ? send(el, \"every\")"));
|
assert!(source.contains("setInterval(() => document.contains(el) ? send(el, \"every\")"));
|
||||||
assert!(source.contains("clearInterval(timers.get(el))"));
|
assert!(source.contains("clearInterval(timers.get(el))"));
|
||||||
}
|
}
|
||||||
@@ -53,9 +56,11 @@ fn runtime_toggles_pending_conventions_around_requests() {
|
|||||||
assert!(source.contains("function showPending(el, on)"));
|
assert!(source.contains("function showPending(el, on)"));
|
||||||
assert!(source.contains("el.getAttribute(\"data-slhx-pending-class\")"));
|
assert!(source.contains("el.getAttribute(\"data-slhx-pending-class\")"));
|
||||||
assert!(source.contains("el.classList.toggle(klass, on)"));
|
assert!(source.contains("el.classList.toggle(klass, on)"));
|
||||||
assert!(source.contains("root.querySelectorAll(\"[data-slhx-indicator]\")"));
|
assert!(source.contains("forEachElement(root, (i) => { if (i.hasAttribute(\"data-slhx-indicator\")) i.hidden = !on; })"));
|
||||||
assert!(source.contains("i.hidden = !on"));
|
assert!(source.contains("i.hidden = !on"));
|
||||||
assert!(source.contains("el.hasAttribute(\"data-slhx-disable-while-pending\")"));
|
assert!(source.contains("el.hasAttribute(\"data-slhx-disable-while-pending\")"));
|
||||||
|
assert!(source.contains("if (isDisableControl(el)) controls.push(el)"));
|
||||||
|
assert!(source.contains("forEachElement(el, (child) => { if (isDisableControl(child)) controls.push(child); })"));
|
||||||
assert!(source.contains("c.disabled = on"));
|
assert!(source.contains("c.disabled = on"));
|
||||||
assert!(source.contains("showPending(target, true)"));
|
assert!(source.contains("showPending(target, true)"));
|
||||||
assert!(source.contains("showPending(target, false)"));
|
assert!(source.contains("showPending(target, false)"));
|
||||||
@@ -95,8 +100,9 @@ fn runtime_clicking_submitter_schedules_form_submit() {
|
|||||||
assert!(source.contains("function formOwner(el)"));
|
assert!(source.contains("function formOwner(el)"));
|
||||||
assert!(source.contains("function formHandleId(form)"));
|
assert!(source.contains("function formHandleId(form)"));
|
||||||
assert!(source.contains("document.getElementById(el.getAttribute(\"form\"))"));
|
assert!(source.contains("document.getElementById(el.getAttribute(\"form\"))"));
|
||||||
assert!(source.contains("const direct = closestInRoot(event.target, root, `[${HID}]`)"));
|
assert!(source.contains("const direct = closestInRoot(event.target, root, (el) => el.hasAttribute(HID))"));
|
||||||
assert!(source.contains("button[type=submit], input[type=submit], button:not([type])"));
|
assert!(source.contains("const submitter = closestInRoot(event.target, root, (el) =>"));
|
||||||
|
assert!(source.contains("el.tagName === \"BUTTON\" && (!el.hasAttribute(\"type\") || el.getAttribute(\"type\") === \"submit\")"));
|
||||||
assert!(source.contains("form.reportValidity && !form.reportValidity()"));
|
assert!(source.contains("form.reportValidity && !form.reportValidity()"));
|
||||||
assert!(source.contains("schedule(form, \"submit\")"));
|
assert!(source.contains("schedule(form, \"submit\")"));
|
||||||
}
|
}
|
||||||
@@ -187,7 +193,8 @@ fn runtime_page_swaps_lowered_slot_ids() {
|
|||||||
let source = slhx_js::RUNTIME_JS;
|
let source = slhx_js::RUNTIME_JS;
|
||||||
|
|
||||||
assert!(source.contains("function replaceLoweredSlots(scope, doc)"));
|
assert!(source.contains("function replaceLoweredSlots(scope, doc)"));
|
||||||
assert!(source.contains("doc.querySelectorAll(\"[data-sid], [data-slot-id]\")"));
|
assert!(source.contains("forEachElement(doc.body || doc, (source)"));
|
||||||
|
assert!(source.contains("const target = generatedTarget(scope, id)"));
|
||||||
assert!(source.contains("target.innerHTML = source.innerHTML"));
|
assert!(source.contains("target.innerHTML = source.innerHTML"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,9 +203,9 @@ fn runtime_keeps_form_field_targets_separate_from_error_targets() {
|
|||||||
let source = slhx_js::RUNTIME_JS;
|
let source = slhx_js::RUNTIME_JS;
|
||||||
|
|
||||||
assert!(source.contains("function fieldTarget(scope, id, field)"));
|
assert!(source.contains("function fieldTarget(scope, id, field)"));
|
||||||
assert!(source.contains("[data-fid=\"${id}\"] [name=\"${escapedField}\"]"));
|
assert!(source.contains("attrEquals(el, \"name\", field) && withinGeneratedForm(el, scope, id)"));
|
||||||
assert!(source.contains("function formErrorTarget(scope, id, field)"));
|
assert!(source.contains("function formErrorTarget(scope, id, field)"));
|
||||||
assert!(source.contains("[data-slhx-error-for=\"${escapedField}\"]"));
|
assert!(source.contains("attrEquals(el, \"data-slhx-error-for\", field) && withinGeneratedForm(el, scope, id)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user