use blitz_dom::NodeId;
use boa_engine::object::JsObject;
use boa_engine::value::JsValue;
use boa_engine::{Context, JsResult};
use super::element::attr_name;
use super::{define_accessor, define_method, dom_ctx, js_str, this_node_id, to_rust_string};
fn css_property_name(js_name: &str) -> String {
let mut css = String::with_capacity(js_name.len() + 2);
for ch in js_name.chars() {
if ch.is_ascii_uppercase() {
css.push('-');
css.push(ch.to_ascii_lowercase());
} else {
css.push(ch);
}
}
css
}
fn is_api_member(name: &str) -> bool {
matches!(
name,
"cssText" | "setProperty" | "removeProperty" | "getPropertyValue" | "constructor"
)
}
fn style_set_trap(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let target = args.first().cloned().unwrap_or_else(JsValue::undefined);
let key = to_rust_string(args.get(1).unwrap_or(&JsValue::undefined()), context)?;
let value = args.get(2).cloned().unwrap_or_else(JsValue::undefined);
if is_api_member(&key) {
if let Some(object) = target.as_object() {
object.set(
js_str(&key).to_property_key(context)?,
value,
false,
context,
)?;
}
return Ok(JsValue::from(true));
}
let ctx = dom_ctx(context)?;
ctx.mark_layout_dirty();
let node_id = this_node_id(&target)?;
let name = css_property_name(&key);
let value = to_rust_string(&value, context)?;
update_style_attr(&ctx, node_id, |decls| {
decls.retain(|(prop, _)| !prop.eq_ignore_ascii_case(&name));
if !value.is_empty() {
decls.push((name.to_ascii_lowercase(), value));
}
});
Ok(JsValue::from(true))
}
fn style_get_trap(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let target = args.first().cloned().unwrap_or_else(JsValue::undefined);
let key = to_rust_string(args.get(1).unwrap_or(&JsValue::undefined()), context)?;
let Some(object) = target.as_object() else {
return Ok(JsValue::undefined());
};
let property = js_str(&key).to_property_key(context)?;
if is_api_member(&key) || object.has_property(property.clone(), context)? {
let value = object.get(property, context)?;
if let Some(function) = value.as_object()
&& function.is_callable()
{
let bind = function.get(boa_engine::js_string!("bind"), context)?;
if let Some(bind) = bind.as_object() {
return bind.call(&value, std::slice::from_ref(&target), context);
}
}
return Ok(value);
}
let ctx = dom_ctx(context)?;
let node_id = this_node_id(&target)?;
let name = css_property_name(&key);
let doc = ctx.doc.borrow();
let style_attr = doc
.get_node(node_id)
.and_then(|node| node.attr(blitz_dom::local_name!("style")))
.unwrap_or_default();
let value = parse_declarations(style_attr)
.into_iter()
.find(|(prop, _)| prop.eq_ignore_ascii_case(&name))
.map(|(_, value)| value)
.unwrap_or_default();
Ok(js_str(&value))
}
pub(crate) fn make_style_object(
proto: JsObject,
node_id: NodeId,
context: &mut Context,
) -> JsResult<JsValue> {
let target = JsObject::from_proto_and_data(Some(proto), super::NodeRef { node_id });
let proxy = boa_engine::object::builtins::JsProxy::builder(target)
.set(style_set_trap)
.get(style_get_trap)
.build(context)?;
Ok(JsValue::from(proxy))
}
pub(crate) fn init_style_proto(proto: &JsObject, context: &mut Context) {
define_accessor(
proto,
"cssText",
Some(get_css_text),
Some(set_css_text),
context,
);
define_method(proto, "setProperty", 2, set_property, context);
define_method(proto, "removeProperty", 1, remove_property, context);
define_method(proto, "getPropertyValue", 1, get_property_value, context);
}
fn get_css_text(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let doc = ctx.doc.borrow();
let css = doc
.get_node(node_id)
.and_then(|node| node.attr(blitz_dom::local_name!("style")))
.unwrap_or_default();
Ok(js_str(css))
}
fn set_css_text(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _t = crate::script_stats::Timed::new(&ctx, "dom:style=");
ctx.mark_layout_dirty();
let node_id = this_node_id(this)?;
let css = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let mut doc = ctx.mutate_doc();
doc.mutate()
.set_attribute(node_id, attr_name("style"), &css);
Ok(JsValue::undefined())
}
fn parse_declarations(style_attr: &str) -> Vec<(String, String)> {
style_attr
.split(';')
.filter_map(|decl| decl.split_once(':'))
.map(|(prop, value)| (prop.trim().to_string(), value.trim().to_string()))
.filter(|(prop, _)| !prop.is_empty())
.collect()
}
fn serialize_declarations(decls: &[(String, String)]) -> String {
decls
.iter()
.map(|(prop, value)| format!("{prop}: {value};"))
.collect::<Vec<_>>()
.join(" ")
}
fn update_style_attr(
ctx: &crate::state::DomCtx,
node_id: NodeId,
f: impl FnOnce(&mut Vec<(String, String)>),
) {
let _t = crate::script_stats::Timed::new(ctx, "dom:style=");
let mut doc = ctx.mutate_doc();
let style_attr = doc
.get_node(node_id)
.and_then(|node| node.attr(blitz_dom::local_name!("style")))
.unwrap_or_default()
.to_string();
let mut decls = parse_declarations(&style_attr);
f(&mut decls);
let new_style = serialize_declarations(&decls);
doc.mutate()
.set_attribute(node_id, attr_name("style"), &new_style);
}
fn set_property(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
ctx.mark_layout_dirty();
let node_id = this_node_id(this)?;
let name = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let value = to_rust_string(args.get(1).unwrap_or(&JsValue::undefined()), context)?;
update_style_attr(&ctx, node_id, |decls| {
decls.retain(|(prop, _)| !prop.eq_ignore_ascii_case(&name));
if !value.is_empty() {
decls.push((name.to_ascii_lowercase(), value));
}
});
Ok(JsValue::undefined())
}
fn remove_property(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
ctx.mark_layout_dirty();
let node_id = this_node_id(this)?;
let name = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let mut removed = String::new();
update_style_attr(&ctx, node_id, |decls| {
if let Some((_, value)) = decls
.iter()
.find(|(prop, _)| prop.eq_ignore_ascii_case(&name))
{
removed = value.clone();
}
decls.retain(|(prop, _)| !prop.eq_ignore_ascii_case(&name));
});
Ok(js_str(&removed))
}
fn get_property_value(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let name = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let doc = ctx.doc.borrow();
let style_attr = doc
.get_node(node_id)
.and_then(|node| node.attr(blitz_dom::local_name!("style")))
.unwrap_or_default();
let value = style_attr
.split(';')
.filter_map(|decl| decl.split_once(':'))
.find(|(prop, _)| prop.trim().eq_ignore_ascii_case(&name))
.map(|(_, value)| value.trim().to_string())
.unwrap_or_default();
Ok(js_str(&value))
}