Expose live computed CSS variables to scripts
This commit is contained in:
1 parent
2343681497
commit
5f67bad4fe
6 files changed
+264
-14
No files matched your search
Vendored
+54
-9
@@ -81,7 +81,19 @@ globalThis.dispatchEvent = function(event) {
|
||||
return !event.defaultPrevented;
|
||||
};
|
||||
|
||||
const _dom = (cmd, a1, a2) => Deno.core.ops.op_dom(cmd, String(a1 ?? ""), String(a2 ?? ""));
|
||||
let _domMutationEpoch = 0;
|
||||
const _DOM_MUTATION_COMMANDS = new Set([
|
||||
"append_child", "insert_before", "remove_child",
|
||||
"set_attribute", "remove_attribute",
|
||||
"set_text_content", "set_inner_html", "set_inner_html_context",
|
||||
"create_element", "create_text_node", "create_comment_node",
|
||||
"create_document_fragment", "clone_node",
|
||||
]);
|
||||
const _dom = (cmd, a1, a2) => {
|
||||
const result = Deno.core.ops.op_dom(cmd, String(a1 ?? ""), String(a2 ?? ""));
|
||||
if (_DOM_MUTATION_COMMANDS.has(cmd)) _domMutationEpoch++;
|
||||
return result;
|
||||
};
|
||||
|
||||
const _nativeFns = new Set();
|
||||
// Exact toString override for members whose native form is not just
|
||||
@@ -2556,6 +2568,23 @@ class Element extends Node {
|
||||
return this.getAttribute("href") || "";
|
||||
}
|
||||
set href(v) { this.setAttribute("href", v); }
|
||||
// HTMLHyperlinkElementUtils / HTMLAnchorElement reflected content
|
||||
// attributes. Real-world locale, routing, and analytics code commonly
|
||||
// enumerates `[hreflang]` links and reads the IDL property rather than
|
||||
// getAttribute(); leaving it undefined aborts the entire component even
|
||||
// though the attribute is present in the DOM.
|
||||
get hreflang() { return this.getAttribute("hreflang") || ""; }
|
||||
set hreflang(v) { this.setAttribute("hreflang", v); }
|
||||
get rel() { return this.getAttribute("rel") || ""; }
|
||||
set rel(v) { this.setAttribute("rel", v); }
|
||||
get target() { return this.getAttribute("target") || ""; }
|
||||
set target(v) { this.setAttribute("target", v); }
|
||||
get download() { return this.getAttribute("download") || ""; }
|
||||
set download(v) { this.setAttribute("download", v); }
|
||||
get ping() { return this.getAttribute("ping") || ""; }
|
||||
set ping(v) { this.setAttribute("ping", v); }
|
||||
get referrerPolicy() { return this.getAttribute("referrerpolicy") || ""; }
|
||||
set referrerPolicy(v) { this.setAttribute("referrerpolicy", v); }
|
||||
// HTMLHyperlinkElementUtils URL-decomposition members, live on <a>/<area>.
|
||||
get protocol() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.protocol : ''; }
|
||||
set protocol(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'protocol', v); }
|
||||
@@ -5863,12 +5892,21 @@ globalThis.getComputedStyle = (el) => {
|
||||
// cascade/layout. Fetch it once per CSSStyleDeclaration proxy: reading a
|
||||
// dozen properties must not trigger a dozen layout/native crossings.
|
||||
let rendered = null;
|
||||
if (typeof Deno.core.ops.op_computed_style === 'function' && el?._nid != null) {
|
||||
try {
|
||||
const raw = Deno.core.ops.op_computed_style(String(el._nid | 0));
|
||||
rendered = raw ? JSON.parse(raw) : null;
|
||||
} catch (e) {}
|
||||
}
|
||||
let renderedEpoch = -1;
|
||||
let computedNames = [];
|
||||
const refreshRendered = () => {
|
||||
if (renderedEpoch === _domMutationEpoch) return;
|
||||
renderedEpoch = _domMutationEpoch;
|
||||
rendered = null;
|
||||
if (typeof Deno.core.ops.op_computed_style === 'function' && el?._nid != null) {
|
||||
try {
|
||||
const raw = Deno.core.ops.op_computed_style(String(el._nid | 0));
|
||||
rendered = raw ? JSON.parse(raw) : null;
|
||||
} catch (e) {}
|
||||
}
|
||||
computedNames = rendered ? Object.keys(rendered) : [];
|
||||
};
|
||||
refreshRendered();
|
||||
// React virtualization libraries (react-window, tanstack-virtual,
|
||||
// react-virtuoso) all compute container dimensions via getComputedStyle.
|
||||
// The defaults table previously returned `auto` for width/height and
|
||||
@@ -5924,6 +5962,7 @@ globalThis.getComputedStyle = (el) => {
|
||||
|
||||
const lookup = (rawProp) => {
|
||||
if (typeof rawProp !== 'string') return '';
|
||||
refreshRendered();
|
||||
const kebab = rawProp.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||
if (rendered && Object.prototype.hasOwnProperty.call(rendered, kebab))
|
||||
return rendered[kebab];
|
||||
@@ -5944,8 +5983,14 @@ globalThis.getComputedStyle = (el) => {
|
||||
if (prop === Symbol.toPrimitive || prop === Symbol.toStringTag) return undefined;
|
||||
if (prop === 'getPropertyValue') return (name) => lookup(name);
|
||||
if (prop === 'getPropertyPriority') return () => '';
|
||||
if (prop === 'item') return (i) => '';
|
||||
if (prop === 'length') return 0;
|
||||
if (prop === 'item') return (i) => {
|
||||
refreshRendered();
|
||||
return computedNames[i | 0] || '';
|
||||
};
|
||||
if (prop === 'length') {
|
||||
refreshRendered();
|
||||
return computedNames.length;
|
||||
}
|
||||
if (prop === 'cssText') return '';
|
||||
if (prop === 'parentRule') return null;
|
||||
// CSSStyleDeclaration's `has` trap intentionally reports every known
|
||||
|
||||
@@ -2473,10 +2473,23 @@ fn op_computed_style(state: &OpState, #[string] nid_str: String) -> String {
|
||||
let nid: u32 = nid_str.parse().unwrap_or(0);
|
||||
let nid = obscura_dom::tree::NodeId::new(nid);
|
||||
let mut gs = shared.borrow_mut();
|
||||
ensure_prepared_render(&mut gs)
|
||||
.and_then(|prepared| prepared.computed_style(nid))
|
||||
.and_then(|snapshot| serde_json::to_string(&snapshot).ok())
|
||||
.unwrap_or_default()
|
||||
let Some(prepared) = ensure_prepared_render(&mut gs) else {
|
||||
return String::new();
|
||||
};
|
||||
let Some(snapshot) = prepared.computed_style(nid) else {
|
||||
return String::new();
|
||||
};
|
||||
let custom = prepared
|
||||
.computed_custom_properties(nid)
|
||||
.unwrap_or_default();
|
||||
let mut object = serde_json::Map::with_capacity(snapshot.len() + custom.len());
|
||||
for (name, value) in snapshot {
|
||||
object.insert(name.to_string(), serde_json::Value::String(value));
|
||||
}
|
||||
for (name, value) in custom {
|
||||
object.insert(name, serde_json::Value::String(value));
|
||||
}
|
||||
serde_json::Value::Object(object).to_string()
|
||||
}
|
||||
|
||||
/// Root scrolling overflow in CSS pixels. The JS CSSOM probes this op only in
|
||||
|
||||
@@ -2168,6 +2168,52 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyperlink_content_attributes_reflect_through_the_idl_surface() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<html><body>
|
||||
<a id="locale" hreflang="en-US" rel="alternate"
|
||||
target="_blank" download="guide.pdf"
|
||||
ping="/audit" referrerpolicy="no-referrer">English</a>
|
||||
</body></html>"#,
|
||||
);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const link = document.getElementById("locale");
|
||||
const initial = [
|
||||
link.hreflang, link.rel, link.target, link.download,
|
||||
link.ping, link.referrerPolicy,
|
||||
link.hreflang.split("-")[1]
|
||||
];
|
||||
link.hreflang = "de-DE";
|
||||
link.referrerPolicy = "origin";
|
||||
return [
|
||||
initial,
|
||||
link.getAttribute("hreflang"),
|
||||
link.getAttribute("referrerpolicy")
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!([
|
||||
[
|
||||
"en-US",
|
||||
"alternate",
|
||||
"_blank",
|
||||
"guide.pdf",
|
||||
"/audit",
|
||||
"no-referrer",
|
||||
"US"
|
||||
],
|
||||
"de-DE",
|
||||
"origin"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[test]
|
||||
fn computed_style_uses_renderer_stylesheet_cascade_and_invalidates() {
|
||||
@@ -2239,6 +2285,78 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[test]
|
||||
fn computed_style_exposes_cascaded_custom_properties_and_invalidates() {
|
||||
let dom = parse_html(
|
||||
r#"<html><head><style>
|
||||
:root { --inherited-space: 17px; --derived-space: var(--inherited-space); }
|
||||
#nav {
|
||||
--r-globalnav-font-size:17px;
|
||||
--local-scale:1.25;
|
||||
font-size:var(--r-globalnav-font-size);
|
||||
}
|
||||
</style></head><body>
|
||||
<nav id="nav"><span id="child"></span></nav>
|
||||
</body></html>"#,
|
||||
);
|
||||
let mut rt = ObscuraJsRuntime::new();
|
||||
rt.set_dom(dom);
|
||||
rt.set_viewport(400.0, 200.0);
|
||||
rt.run_page_init();
|
||||
|
||||
assert_eq!(
|
||||
rt.evaluate(
|
||||
r#"
|
||||
const nav = document.getElementById("nav");
|
||||
const child = document.getElementById("child");
|
||||
const navStyle = getComputedStyle(nav);
|
||||
const childStyle = getComputedStyle(child);
|
||||
let enumeratesBase = false;
|
||||
for (let i = 0; i < navStyle.length; i++) {
|
||||
if (navStyle.item(i) === "--r-globalnav-font-size")
|
||||
enumeratesBase = true;
|
||||
}
|
||||
return [
|
||||
navStyle.fontSize,
|
||||
navStyle.getPropertyValue("--r-globalnav-font-size"),
|
||||
parseInt(navStyle.fontSize) /
|
||||
parseInt(navStyle.getPropertyValue("--r-globalnav-font-size")),
|
||||
navStyle.getPropertyValue("--inherited-space"),
|
||||
navStyle.getPropertyValue("--derived-space"),
|
||||
navStyle.getPropertyValue("--local-scale"),
|
||||
childStyle.getPropertyValue("--inherited-space"),
|
||||
childStyle.getPropertyValue("--local-scale"),
|
||||
enumeratesBase
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
serde_json::json!([
|
||||
"17px", "17px", 1,
|
||||
"17px", "17px", "1.25", "17px", "1.25", true
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rt.evaluate(
|
||||
r#"
|
||||
const nav = document.getElementById("nav");
|
||||
const computed = getComputedStyle(nav);
|
||||
nav.style.setProperty("--inherited-space", "23px");
|
||||
nav.style.fontSize = "19px";
|
||||
return [
|
||||
computed.fontSize,
|
||||
computed.getPropertyValue("--inherited-space"),
|
||||
computed.getPropertyValue("--derived-space")
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
serde_json::json!(["19px", "23px", "17px"])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn idle_event_loop_flushes_resolved_promise_continuations() {
|
||||
let mut rt = setup_runtime("<html><body><div id='state'>pending</div></body></html>");
|
||||
|
||||
Reference in new issue
Block a user