use blitz_dom::NodeId;
use blitz_dom::{LocalName, QualName};
use boa_engine::object::{JsObject, ObjectInitializer, WeakJsObject, builtins::JsProxyBuilder};
use boa_engine::property::{Attribute as PropAttribute, PropertyKey};
use boa_engine::value::JsValue;
use boa_engine::{Context, Finalize, JsData, JsNativeError, JsResult, Trace, js_string};
use super::{
define_accessor, define_method, dom_ctx, js_str, node_wrapper, this_node_id, to_rust_string,
};
use crate::state::DomCtx;
pub(crate) fn attr_name(local: &str) -> QualName {
QualName::new(None, markup5ever::ns!(), LocalName::from(local))
}
pub(crate) fn init_element_proto(proto: &JsObject, context: &mut Context) {
define_accessor(proto, "tagName", Some(tag_name), None, context);
define_accessor(proto, "localName", Some(local_name), None, context);
define_accessor(proto, "namespaceURI", Some(namespace_uri), None, context);
define_accessor(proto, "id", Some(get_id), Some(set_id), context);
define_accessor(proto, "src", Some(get_src), Some(set_src), context);
define_accessor(proto, "href", Some(get_href), Some(set_href), context);
define_accessor(proto, "target", Some(get_target), Some(set_target), context);
init_hyperlink_utils(proto, context);
define_accessor(
proto,
"className",
Some(get_class_name),
Some(set_class_name),
context,
);
define_accessor(proto, "value", Some(get_value), Some(set_value), context);
define_accessor(
proto,
"checked",
Some(get_checked),
Some(set_checked),
context,
);
define_accessor(
proto,
"selectedIndex",
Some(get_selected_index),
Some(set_selected_index),
context,
);
define_accessor(
proto,
"selected",
Some(get_selected),
Some(set_selected),
context,
);
define_accessor(proto, "options", Some(get_options), None, context);
define_accessor(
proto,
"disabled",
Some(get_disabled),
Some(set_disabled),
context,
);
define_accessor(
proto,
"placeholder",
Some(get_placeholder),
Some(set_placeholder),
context,
);
define_accessor(proto, "type", Some(get_type), Some(set_type), context);
define_accessor(
proto,
"autofocus",
Some(get_autofocus),
Some(set_autofocus),
context,
);
define_accessor(proto, "style", Some(get_style), Some(set_style), context);
define_accessor(proto, "dataset", Some(get_dataset), None, context);
define_accessor(proto, "classList", Some(get_class_list), None, context);
define_accessor(
proto,
"innerHTML",
Some(get_inner_html),
Some(set_inner_html),
context,
);
define_accessor(
proto,
"outerHTML",
Some(get_outer_html),
Some(super::doma::tree::set_outer_html),
context,
);
define_accessor(proto, "children", Some(children), None, context);
define_accessor(
proto,
"content",
Some(get_template_content),
Some(set_content),
context,
);
define_accessor(
proto,
"scrollLeft",
Some(get_scroll_left),
Some(set_scroll_left),
context,
);
define_accessor(
proto,
"scrollTop",
Some(get_scroll_top),
Some(set_scroll_top),
context,
);
define_accessor(proto, "scrollWidth", Some(get_scroll_width), None, context);
define_accessor(
proto,
"scrollHeight",
Some(get_scroll_height),
None,
context,
);
define_accessor(proto, "clientWidth", Some(get_client_width), None, context);
define_accessor(
proto,
"clientHeight",
Some(get_client_height),
None,
context,
);
define_accessor(proto, "offsetWidth", Some(get_offset_width), None, context);
define_accessor(
proto,
"offsetHeight",
Some(get_offset_height),
None,
context,
);
define_accessor(proto, "offsetLeft", Some(get_offset_left), None, context);
define_accessor(proto, "offsetTop", Some(get_offset_top), None, context);
define_method(proto, "getAttribute", 1, get_attribute, context);
define_method(proto, "setAttribute", 2, set_attribute, context);
define_method(proto, "removeAttribute", 1, remove_attribute, context);
define_method(proto, "hasAttribute", 1, has_attribute, context);
define_method(proto, "focus", 0, focus, context);
define_method(proto, "blur", 0, blur, context);
define_method(proto, "scrollIntoView", 1, scroll_into_view, context);
define_method(
proto,
"getBoundingClientRect",
0,
get_bounding_client_rect,
context,
);
define_method(proto, "querySelector", 1, query_selector, context);
define_method(proto, "querySelectorAll", 1, query_selector_all, context);
define_method(proto, "matches", 1, matches_selector, context);
define_method(proto, "webkitMatchesSelector", 1, matches_selector, context);
define_method(
proto,
"insertAdjacentElement",
2,
super::doma::tree::insert_adjacent_element,
context,
);
define_method(
proto,
"insertAdjacentText",
2,
super::doma::tree::insert_adjacent_text,
context,
);
define_method(
proto,
"insertAdjacentHTML",
2,
super::doma::tree::insert_adjacent_html,
context,
);
define_method(proto, "closest", 1, closest, context);
define_method(proto, "setPointerCapture", 1, set_pointer_capture, context);
define_method(
proto,
"releasePointerCapture",
1,
release_pointer_capture,
context,
);
define_method(proto, "hasPointerCapture", 1, has_pointer_capture, context);
define_method(proto, "getContext", 1, get_context, context);
}
fn get_context(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::null())
}
fn pointer_id_arg(args: &[JsValue], context: &mut Context) -> JsResult<u64> {
let value = args
.first()
.unwrap_or(&JsValue::undefined())
.to_number(context)?;
if !value.is_finite() || value < 0.0 {
return Err(JsNativeError::typ()
.with_message("pointerId must be a non-negative finite number")
.into());
}
Ok(value as u64)
}
fn set_pointer_capture(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let pointer_id = pointer_id_arg(args, context)?;
ctx.state
.borrow_mut()
.pointer_capture
.insert(pointer_id, node_id);
Ok(JsValue::undefined())
}
fn release_pointer_capture(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let pointer_id = pointer_id_arg(args, context)?;
let mut state = ctx.state.borrow_mut();
if state.pointer_capture.get(&pointer_id) == Some(&node_id) {
state.pointer_capture.remove(&pointer_id);
}
Ok(JsValue::undefined())
}
fn has_pointer_capture(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let pointer_id = pointer_id_arg(args, context)?;
Ok(JsValue::from(
ctx.state.borrow().pointer_capture.get(&pointer_id) == Some(&node_id),
))
}
fn read_attr(ctx: &DomCtx, node_id: NodeId, name: &str) -> Option<String> {
let doc = ctx.doc.borrow();
let node = doc.get_node(node_id)?;
let element = node.element_data()?;
let (prefix, local) = name
.split_once(':')
.map_or((None, name), |(prefix, local)| (Some(prefix), local));
element
.attrs()
.iter()
.find(|attr| {
(&*attr.name.local == local && attr.name.prefix.as_deref() == prefix)
|| (attr.name.prefix.is_none() && &*attr.name.local == name)
})
.map(|attr| attr.value.to_string())
}
fn write_attr(ctx: &DomCtx, node_id: NodeId, name: &str, value: &str) {
let _t = crate::script_stats::Timed::new(ctx, "dom:attr=");
let mut doc = ctx.mutate_doc();
let qualified_name = existing_attr_name(&doc, node_id, name).unwrap_or_else(|| attr_name(name));
doc.mutate().set_attribute(node_id, qualified_name, value);
}
fn clear_attr(ctx: &DomCtx, node_id: NodeId, name: &str) {
let _t = crate::script_stats::Timed::new(ctx, "dom:attr-remove");
let mut doc = ctx.mutate_doc();
let qualified_name = existing_attr_name(&doc, node_id, name).unwrap_or_else(|| attr_name(name));
doc.mutate().clear_attribute(node_id, qualified_name);
}
fn existing_attr_name(
doc: &blitz_dom::BaseDocument,
node_id: NodeId,
name: &str,
) -> Option<QualName> {
let (prefix, local) = name
.split_once(':')
.map_or((None, name), |(prefix, local)| (Some(prefix), local));
doc.get_node(node_id)?
.element_data()?
.attrs()
.iter()
.find(|attr| {
(&*attr.name.local == local && attr.name.prefix.as_deref() == prefix)
|| (attr.name.prefix.is_none() && &*attr.name.local == name)
})
.map(|attr| attr.name.clone())
}
fn attr_getter(name: &str, this: &JsValue, context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
Ok(js_str(&read_attr(&ctx, node_id, name).unwrap_or_default()))
}
fn attr_setter(
name: &str,
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let value = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
write_attr(&ctx, node_id, name, &value);
Ok(JsValue::undefined())
}
fn tag_name(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 name = doc
.get_node(node_id)
.and_then(|node| node.qualified_element_name())
.unwrap_or_default();
Ok(js_str(&name))
}
fn local_name(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 name = doc
.get_node(node_id)
.and_then(|node| node.element_data())
.map(|element| element.name.local.to_string())
.unwrap_or_default();
Ok(js_str(&name))
}
fn element_namespace_uri(ctx: &DomCtx, node_id: NodeId) -> String {
let doc = ctx.doc.borrow();
doc.get_node(node_id)
.and_then(|node| node.element_data())
.map(|element| element.name.ns.to_string())
.unwrap_or_default()
}
fn is_svg_element(ctx: &DomCtx, node_id: NodeId) -> bool {
element_namespace_uri(ctx, node_id) == "http://www.w3.org/2000/svg"
}
fn is_anchor(ctx: &DomCtx, node_id: NodeId) -> bool {
ctx.doc
.borrow()
.get_node(node_id)
.and_then(|node| node.element_data())
.is_some_and(|element| &*element.name.local == "a")
}
fn namespace_uri(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
Ok(js_str(&element_namespace_uri(&ctx, node_id)))
}
fn children(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let child_ids: Vec<NodeId> = {
let doc = ctx.doc.borrow();
doc.get_node(node_id)
.map(|node| {
node.children
.iter()
.copied()
.filter(|child_id| {
doc.get_node(*child_id)
.is_some_and(|child| child.is_element())
})
.collect()
})
.unwrap_or_default()
};
let wrappers: Vec<JsValue> = child_ids
.into_iter()
.map(|child_id| node_wrapper(&ctx, child_id, context).into())
.collect();
Ok(boa_engine::object::builtins::JsArray::from_iter(wrappers, context).into())
}
fn get_attribute(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)?
.to_ascii_lowercase();
match read_attr(&ctx, node_id, &name) {
Some(value) => Ok(js_str(&value)),
None => Ok(JsValue::null()),
}
}
fn set_attribute(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)?
.to_ascii_lowercase();
let value = to_rust_string(args.get(1).unwrap_or(&JsValue::undefined()), context)?;
write_attr(&ctx, node_id, &name, &value);
Ok(JsValue::undefined())
}
fn remove_attribute(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)?
.to_ascii_lowercase();
clear_attr(&ctx, node_id, &name);
Ok(JsValue::undefined())
}
fn has_attribute(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)?
.to_ascii_lowercase();
Ok(JsValue::from(read_attr(&ctx, node_id, &name).is_some()))
}
fn get_id(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = args;
attr_getter("id", this, context)
}
fn set_id(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
attr_setter("id", this, args, context)
}
fn get_src(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = args;
attr_getter("src", this, context)
}
fn set_src(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
attr_setter("src", this, args, context)
}
fn svg_animated_string(
node_id: NodeId,
getter: fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue>,
setter: fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue>,
context: &mut Context,
) -> JsValue {
let object = JsObject::from_proto_and_data(
Some(context.intrinsics().constructors().object().prototype()),
super::NodeRef { node_id },
);
define_accessor(&object, "baseVal", Some(getter), Some(setter), context);
define_accessor(&object, "animVal", Some(getter), None, context);
object.into()
}
fn get_svg_href_string(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let _ = args;
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let value = read_attr(&ctx, node_id, "href")
.or_else(|| read_attr(&ctx, node_id, "xlink:href"))
.unwrap_or_default();
Ok(js_str(&value))
}
fn get_svg_target_string(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let _ = args;
attr_getter("target", this, context)
}
fn get_href(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = args;
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
if is_svg_element(&ctx, node_id) {
if !is_anchor(&ctx, node_id) {
return Ok(JsValue::undefined());
}
return Ok(svg_animated_string(
node_id,
get_svg_href_string,
set_href,
context,
));
}
let Some(attr) = read_attr(&ctx, node_id, "href") else {
return Ok(JsValue::undefined());
};
let doc = ctx.doc.borrow();
let resolved = doc
.url()
.join(&attr)
.map(|url| url.to_string())
.unwrap_or(attr);
Ok(js_str(&resolved))
}
fn set_href(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
attr_setter("href", this, args, context)
}
fn get_target(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = args;
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
if is_svg_element(&ctx, node_id) {
if !is_anchor(&ctx, node_id) {
return Ok(JsValue::undefined());
}
return Ok(svg_animated_string(
node_id,
get_svg_target_string,
set_target,
context,
));
}
attr_getter("target", this, context)
}
fn set_target(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
attr_setter("target", this, args, context)
}
fn get_class_name(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = args;
attr_getter("class", this, context)
}
fn set_class_name(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
attr_setter("class", this, args, context)
}
fn get_placeholder(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = args;
attr_getter("placeholder", this, context)
}
fn set_placeholder(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
attr_setter("placeholder", this, args, context)
}
fn get_type(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = args;
attr_getter("type", this, context)
}
fn set_type(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
attr_setter("type", this, args, context)
}
fn get_autofocus(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
Ok(JsValue::from(
read_attr(&ctx, node_id, "autofocus").is_some(),
))
}
fn set_autofocus(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let value = args.first().map(JsValue::to_boolean).unwrap_or(false);
if value {
write_attr(&ctx, node_id, "autofocus", "");
} else {
clear_attr(&ctx, node_id, "autofocus");
}
Ok(JsValue::undefined())
}
fn is_select(ctx: &DomCtx, node_id: NodeId) -> bool {
let doc = ctx.doc.borrow();
doc.get_node(node_id).is_some_and(|node| {
node.data
.is_element_with_tag_name(&blitz_dom::local_name!("select"))
})
}
fn get_value(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
if is_select(&ctx, node_id) {
let value = ctx.doc.borrow().select_value(node_id);
return Ok(js_str(&value));
}
let value = {
let doc = ctx.doc.borrow();
doc.get_node(node_id)
.and_then(|node| node.element_data())
.map(|element| match element.text_input_data() {
Some(input_data) => input_data.editor.text().to_string(),
None => element
.attrs()
.iter()
.find(|attr| &*attr.name.local == "value")
.map(|attr| attr.value.to_string())
.unwrap_or_default(),
})
.unwrap_or_default()
};
Ok(js_str(&value))
}
fn set_value(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
if is_select(&ctx, node_id) {
let value = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
select_by_value(&ctx, node_id, &value);
return Ok(JsValue::undefined());
}
attr_setter("value", this, args, context)
}
fn select_by_value(ctx: &DomCtx, node_id: NodeId, value: &str) {
let target = {
let doc = ctx.doc.borrow();
doc.select_options(node_id)
.into_iter()
.position(|option_id| doc.option_value(option_id) == value)
};
let mut doc = ctx.mutate_doc();
let changed = match target {
Some(index) => doc.set_select_selected_index(node_id, index),
None => doc
.get_node_mut(node_id)
.and_then(|node| node.data.downcast_element_mut())
.and_then(|el| el.select_data_mut())
.is_some_and(|data| data.clear_selection()),
};
if changed {
doc.snapshot_node(node_id);
}
}
fn get_selected_index(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let index = ctx
.doc
.borrow()
.select_selected_index(node_id)
.map(|index| index as i64)
.unwrap_or(-1);
Ok(JsValue::from(index))
}
fn set_selected_index(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let index = args
.first()
.map(|value| value.to_number(context))
.transpose()?
.unwrap_or(-1.0);
let mut doc = ctx.mutate_doc();
let changed = if index < 0.0 {
doc.get_node_mut(node_id)
.and_then(|node| node.data.downcast_element_mut())
.and_then(|el| el.select_data_mut())
.is_some_and(|data| data.clear_selection())
} else {
doc.set_select_selected_index(node_id, index as usize)
};
if changed {
doc.snapshot_node(node_id);
}
Ok(JsValue::undefined())
}
fn get_options(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let option_ids = ctx.doc.borrow().select_options(node_id);
let wrappers: Vec<JsValue> = option_ids
.into_iter()
.map(|option_id| node_wrapper(&ctx, option_id, context).into())
.collect();
Ok(boa_engine::object::builtins::JsArray::from_iter(wrappers, context).into())
}
fn get_selected(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let selected = ctx.doc.borrow().option_is_selected(node_id);
Ok(JsValue::from(selected))
}
fn set_selected(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let selected = args.first().map(JsValue::to_boolean).unwrap_or(false);
let owner = {
let doc = ctx.doc.borrow();
doc.option_owner_select(node_id).map(|select_id| {
let index = doc
.select_options(select_id)
.into_iter()
.position(|id| id == node_id);
(select_id, index)
})
};
let Some((select_id, Some(index))) = owner else {
if selected {
write_attr(&ctx, node_id, "selected", "");
} else {
clear_attr(&ctx, node_id, "selected");
}
return Ok(JsValue::undefined());
};
let mut doc = ctx.mutate_doc();
let has_live_state = doc
.get_node(select_id)
.and_then(|node| node.data.downcast_element())
.is_some_and(|el| el.select_data().is_some());
if has_live_state {
let changed = if selected {
doc.set_select_selected_index(select_id, index)
} else {
doc.get_node_mut(select_id)
.and_then(|node| node.data.downcast_element_mut())
.and_then(|el| el.select_data_mut())
.is_some_and(|data| data.set_selected(index, false))
};
if changed {
doc.snapshot_node(node_id);
doc.snapshot_node(select_id);
}
return Ok(JsValue::undefined());
}
drop(doc);
if selected {
write_attr(&ctx, node_id, "selected", "");
} else {
clear_attr(&ctx, node_id, "selected");
}
Ok(JsValue::undefined())
}
fn get_checked(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let checked = {
let doc = ctx.doc.borrow();
doc.get_node(node_id)
.and_then(|node| node.element_data())
.map(|element| {
element
.checkbox_input_checked()
.unwrap_or_else(|| element.attr(blitz_dom::local_name!("checked")).is_some())
})
.unwrap_or(false)
};
Ok(JsValue::from(checked))
}
fn set_checked(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let checked = args.first().map(JsValue::to_boolean).unwrap_or(false);
let has_live_state = {
let mut doc = ctx.mutate_doc();
match doc
.get_node_mut(node_id)
.and_then(|node| node.data.downcast_element_mut())
.and_then(|element| element.checkbox_input_checked_mut())
{
Some(is_checked) => {
let changed = *is_checked != checked;
*is_checked = checked;
if changed {
doc.snapshot_node(node_id);
}
true
}
None => false,
}
};
if !has_live_state {
if checked {
write_attr(&ctx, node_id, "checked", "");
} else {
clear_attr(&ctx, node_id, "checked");
}
}
Ok(JsValue::undefined())
}
fn get_disabled(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
Ok(JsValue::from(
read_attr(&ctx, node_id, "disabled").is_some(),
))
}
fn set_disabled(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let disabled = args.first().map(JsValue::to_boolean).unwrap_or(false);
if disabled {
write_attr(&ctx, node_id, "disabled", "");
} else {
clear_attr(&ctx, node_id, "disabled");
}
Ok(JsValue::undefined())
}
pub(crate) fn install_details(proto: &JsObject, context: &mut Context) {
define_accessor(proto, "open", Some(get_open), Some(set_open), context);
}
fn get_open(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
Ok(JsValue::from(read_attr(&ctx, node_id, "open").is_some()))
}
fn set_open(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
if args.first().is_some_and(JsValue::to_boolean) {
write_attr(&ctx, node_id, "open", "");
} else {
clear_attr(&ctx, node_id, "open");
}
Ok(JsValue::undefined())
}
pub(crate) fn deliver_details_toggles(ctx: &DomCtx, context: &mut Context) -> bool {
let toggles = ctx.doc.borrow_mut().take_details_toggles();
if toggles.is_empty() {
return false;
}
let Some(constructor) = context
.global_object()
.get(js_string!("ToggleEvent"), context)
.ok()
.and_then(|value| value.as_constructor())
else {
return false;
};
let state = |open: bool| js_string!(if open { "open" } else { "closed" });
for (node_id, was_open) in toggles {
let Some(open) = ctx
.doc
.borrow()
.get_node(node_id)
.and_then(|node| node.element_data())
.map(|element| element.attr(blitz_dom::local_name!("open")).is_some())
else {
continue;
};
let init = JsObject::with_object_proto(context.intrinsics());
let fired = init
.set(js_string!("oldState"), state(was_open), false, context)
.and_then(|_| init.set(js_string!("newState"), state(open), false, context))
.and_then(|_| {
constructor.construct(&[js_string!("toggle").into(), init.into()], None, context)
})
.and_then(|event| {
let target = super::node_wrapper(ctx, node_id, context);
let dispatch = target.get(js_string!("dispatchEvent"), context)?;
let dispatch = dispatch.as_callable().ok_or_else(|| {
JsNativeError::typ().with_message("dispatchEvent is not callable")
})?;
dispatch.call(&target.into(), &[event.into()], context)
});
if let Err(error) = fired {
eprintln!("details toggle event: {error}");
}
}
true
}
#[derive(Trace, Finalize, JsData)]
struct DatasetRef {
#[unsafe_ignore_trace]
node_id: NodeId,
}
fn dataset_ref(args: &[JsValue]) -> JsResult<NodeId> {
args.first()
.and_then(JsValue::as_object)
.and_then(|target| target.downcast_ref::<DatasetRef>().map(|data| data.node_id))
.ok_or_else(|| {
JsNativeError::typ()
.with_message("dataset proxy target is invalid")
.into()
})
}
fn dataset_key(value: &JsValue, context: &mut Context) -> JsResult<Option<String>> {
Ok(match value.to_property_key(context)? {
PropertyKey::String(key) => Some(key.to_std_string_lossy()),
PropertyKey::Index(key) => Some(key.get().to_string()),
PropertyKey::Symbol(_) => None,
})
}
fn dataset_attr_name(key: &str) -> String {
let mut name = String::with_capacity(key.len() + 5);
name.push_str("data-");
for ch in key.chars() {
if ch.is_ascii_uppercase() {
name.push('-');
name.push(ch.to_ascii_lowercase());
} else {
name.push(ch);
}
}
name
}
fn dataset_property_name(attr: &str) -> Option<String> {
let suffix = attr.strip_prefix("data-")?;
let mut key = String::with_capacity(suffix.len());
let mut chars = suffix.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '-' && chars.peek().is_some_and(char::is_ascii_lowercase) {
key.push(chars.next().expect("peeked character").to_ascii_uppercase());
} else {
key.push(ch);
}
}
Some(key)
}
fn dataset_get(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(key) = dataset_key(args.get(1).unwrap_or(&JsValue::undefined()), context)? else {
return Ok(JsValue::undefined());
};
let ctx = dom_ctx(context)?;
let node_id = dataset_ref(args)?;
Ok(read_attr(&ctx, node_id, &dataset_attr_name(&key))
.map_or_else(JsValue::undefined, |value| js_str(&value)))
}
fn dataset_set(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(key) = dataset_key(args.get(1).unwrap_or(&JsValue::undefined()), context)? else {
return Ok(JsValue::from(false));
};
let value = to_rust_string(args.get(2).unwrap_or(&JsValue::undefined()), context)?;
let ctx = dom_ctx(context)?;
let node_id = dataset_ref(args)?;
write_attr(&ctx, node_id, &dataset_attr_name(&key), &value);
Ok(JsValue::from(true))
}
fn dataset_delete(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(key) = dataset_key(args.get(1).unwrap_or(&JsValue::undefined()), context)? else {
return Ok(JsValue::from(true));
};
let ctx = dom_ctx(context)?;
let node_id = dataset_ref(args)?;
clear_attr(&ctx, node_id, &dataset_attr_name(&key));
Ok(JsValue::from(true))
}
fn dataset_has(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(key) = dataset_key(args.get(1).unwrap_or(&JsValue::undefined()), context)? else {
return Ok(JsValue::from(false));
};
let ctx = dom_ctx(context)?;
let node_id = dataset_ref(args)?;
Ok(JsValue::from(
read_attr(&ctx, node_id, &dataset_attr_name(&key)).is_some(),
))
}
fn dataset_own_keys(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = dataset_ref(args)?;
let keys: Vec<JsValue> = {
let doc = ctx.doc.borrow();
doc.get_node(node_id)
.and_then(|node| node.element_data())
.map(|element| {
element
.attrs()
.iter()
.filter_map(|attr| dataset_property_name(&attr.name.local))
.map(|key| js_str(&key))
.collect()
})
.unwrap_or_default()
};
Ok(boa_engine::object::builtins::JsArray::from_iter(keys, context).into())
}
fn dataset_descriptor(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(key) = dataset_key(args.get(1).unwrap_or(&JsValue::undefined()), context)? else {
return Ok(JsValue::undefined());
};
let ctx = dom_ctx(context)?;
let node_id = dataset_ref(args)?;
let Some(value) = read_attr(&ctx, node_id, &dataset_attr_name(&key)) else {
return Ok(JsValue::undefined());
};
Ok(ObjectInitializer::new(context)
.property(js_string!("value"), js_str(&value), PropAttribute::all())
.property(js_string!("writable"), true, PropAttribute::all())
.property(js_string!("enumerable"), true, PropAttribute::all())
.property(js_string!("configurable"), true, PropAttribute::all())
.build()
.into())
}
fn get_dataset(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
if let Some(dataset) = ctx
.state
.borrow()
.dataset_wrappers
.get(&node_id)
.and_then(WeakJsObject::upgrade)
{
return Ok(dataset.into());
}
let target = JsObject::from_proto_and_data(
Some(context.intrinsics().constructors().object().prototype()),
DatasetRef { node_id },
);
let dataset: JsObject = JsProxyBuilder::new(target)
.get(dataset_get)
.set(dataset_set)
.delete_property(dataset_delete)
.has(dataset_has)
.own_keys(dataset_own_keys)
.get_own_property_descriptor(dataset_descriptor)
.build(context)?
.into();
ctx.state
.borrow_mut()
.dataset_wrappers
.insert(node_id, dataset.downgrade());
Ok(dataset.into())
}
fn class_tokens(ctx: &DomCtx, node_id: NodeId) -> Vec<String> {
read_attr(ctx, node_id, "class")
.unwrap_or_default()
.split_ascii_whitespace()
.map(str::to_owned)
.collect()
}
fn write_class_tokens(ctx: &DomCtx, node_id: NodeId, tokens: &[String]) {
write_attr(ctx, node_id, "class", &tokens.join(" "));
}
fn class_token(value: &JsValue, context: &mut Context) -> JsResult<String> {
let token = to_rust_string(value, context)?;
if token.is_empty() {
return Err(crate::dom_exception::error(
"SyntaxError",
"classList token must not be empty",
context,
));
}
if token.chars().any(|ch| ch.is_ascii_whitespace()) {
return Err(crate::dom_exception::error(
"InvalidCharacterError",
"classList token contains ASCII whitespace",
context,
));
}
Ok(token)
}
fn class_list_length(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
Ok(JsValue::from(
class_tokens(&ctx, this_node_id(this)?).len() as u32
))
}
fn class_list_value(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
Ok(js_str(
&read_attr(&ctx, this_node_id(this)?, "class").unwrap_or_default(),
))
}
fn set_class_list_value(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let value = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
write_attr(&ctx, this_node_id(this)?, "class", &value);
Ok(JsValue::undefined())
}
fn class_list_item(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let index = args
.first()
.unwrap_or(&JsValue::undefined())
.to_u32(context)? as usize;
let ctx = dom_ctx(context)?;
Ok(class_tokens(&ctx, this_node_id(this)?)
.get(index)
.map_or_else(JsValue::null, |token| js_str(token)))
}
fn class_list_contains(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let token = class_token(args.first().unwrap_or(&JsValue::undefined()), context)?;
let ctx = dom_ctx(context)?;
Ok(JsValue::from(
class_tokens(&ctx, this_node_id(this)?).contains(&token),
))
}
fn class_list_add(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let tokens_to_add = args
.iter()
.map(|value| class_token(value, context))
.collect::<JsResult<Vec<_>>>()?;
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let mut tokens = class_tokens(&ctx, node_id);
for token in tokens_to_add {
if !tokens.contains(&token) {
tokens.push(token);
}
}
write_class_tokens(&ctx, node_id, &tokens);
Ok(JsValue::undefined())
}
fn class_list_remove(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let tokens_to_remove = args
.iter()
.map(|value| class_token(value, context))
.collect::<JsResult<Vec<_>>>()?;
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let mut tokens = class_tokens(&ctx, node_id);
tokens.retain(|token| !tokens_to_remove.contains(token));
write_class_tokens(&ctx, node_id, &tokens);
Ok(JsValue::undefined())
}
fn class_list_toggle(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let token = class_token(args.first().unwrap_or(&JsValue::undefined()), context)?;
let force = args.get(1).map(JsValue::to_boolean);
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let mut tokens = class_tokens(&ctx, node_id);
let present = tokens.contains(&token);
let retain = force.unwrap_or(!present);
if retain && !present {
tokens.push(token);
} else if !retain && present {
tokens.retain(|item| item != &token);
}
write_class_tokens(&ctx, node_id, &tokens);
Ok(JsValue::from(retain))
}
fn class_list_replace(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let old = class_token(args.first().unwrap_or(&JsValue::undefined()), context)?;
let new = class_token(args.get(1).unwrap_or(&JsValue::undefined()), context)?;
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let mut tokens = class_tokens(&ctx, node_id);
let Some(index) = tokens.iter().position(|token| token == &old) else {
return Ok(JsValue::from(false));
};
if old != new {
tokens[index] = new;
let mut seen = std::collections::HashSet::new();
tokens.retain(|token| seen.insert(token.clone()));
}
write_class_tokens(&ctx, node_id, &tokens);
Ok(JsValue::from(true))
}
fn class_list_to_string(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
class_list_value(this, args, context)
}
fn get_class_list(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
if let Some(class_list) = ctx
.state
.borrow()
.class_list_wrappers
.get(&node_id)
.and_then(WeakJsObject::upgrade)
{
return Ok(class_list.into());
}
let class_list = JsObject::from_proto_and_data(
Some(context.intrinsics().constructors().object().prototype()),
super::NodeRef { node_id },
);
define_accessor(
&class_list,
"length",
Some(class_list_length),
None,
context,
);
define_accessor(
&class_list,
"value",
Some(class_list_value),
Some(set_class_list_value),
context,
);
define_method(&class_list, "item", 1, class_list_item, context);
define_method(&class_list, "contains", 1, class_list_contains, context);
define_method(&class_list, "add", 1, class_list_add, context);
define_method(&class_list, "remove", 1, class_list_remove, context);
define_method(&class_list, "toggle", 1, class_list_toggle, context);
define_method(&class_list, "replace", 2, class_list_replace, context);
define_method(&class_list, "toString", 0, class_list_to_string, context);
ctx.state
.borrow_mut()
.class_list_wrappers
.insert(node_id, class_list.downgrade());
Ok(class_list.into())
}
fn get_style(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let proto = ctx.state.borrow().protos().style.clone();
super::style::make_style_object(proto, node_id, context)
}
fn set_style(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let declaration = get_style(this, &[], context)?
.as_object()
.expect("inline style declaration is an object");
declaration.set(
js_string!("cssText"),
args.first().cloned().unwrap_or_else(JsValue::undefined),
true,
context,
)?;
Ok(JsValue::undefined())
}
fn set_content(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
if is_html_element_named(&ctx, node_id, "meta") {
return attr_setter("content", this, args, context);
}
Ok(JsValue::undefined())
}
fn is_html_element_named(ctx: &DomCtx, node_id: NodeId, local: &str) -> bool {
ctx.doc
.borrow()
.get_node(node_id)
.and_then(|node| node.element_data())
.is_some_and(|element| {
element.name.ns == markup5ever::ns!(html) && &*element.name.local == local
})
}
fn get_template_content(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let contents = ctx
.doc
.borrow()
.get_node(node_id)
.and_then(|node| node.element_data())
.and_then(|element| element.template_contents);
let Some(contents) = contents else {
if is_html_element_named(&ctx, node_id, "meta") {
return attr_getter("content", this, context);
}
return Ok(JsValue::undefined());
};
let fragment = node_wrapper(&ctx, contents, context);
if let Some(template) = this.as_object() {
super::define_value(
&template,
"__blitz_internal_template_content__",
fragment.clone().into(),
context,
);
}
Ok(fragment.into())
}
pub(crate) fn get_inner_html(
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 mut html = String::new();
if let Some(node) = doc.get_node(node_id) {
node.write_children_html(&mut html);
}
Ok(js_str(&html))
}
pub(crate) fn set_inner_html(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let html = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let target = ctx
.doc
.borrow()
.get_node(node_id)
.and_then(|node| node.element_data())
.and_then(|element| element.template_contents)
.unwrap_or(node_id);
let children = ctx
.doc
.borrow()
.get_node(target)
.map(|node| node.children.to_vec())
.unwrap_or_default();
for child_id in children {
super::remove_and_free_node(&ctx, child_id, context);
}
ctx.mutate_doc().mutate().set_inner_html(node_id, &html);
Ok(JsValue::undefined())
}
fn get_outer_html(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 mut html = String::new();
if let Some(node) = doc.get_node(node_id) {
node.write_html(&mut html);
}
Ok(js_str(&html))
}
fn focus(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
ctx.mutate_doc().set_focus_to(node_id);
Ok(JsValue::undefined())
}
fn blur(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _ = this_node_id(this)?;
ctx.mutate_doc().clear_focus();
Ok(JsValue::undefined())
}
fn scroll_into_view(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
ctx.flush_layout();
ctx.mutate_doc().scroll_to_node(node_id);
Ok(JsValue::undefined())
}
fn get_scroll_left(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let value = ctx
.doc
.borrow()
.get_node(node_id)
.map(|node| match node.primary_styles() {
Some(styles) => styles.effective_zoom.unzoom(node.scroll_offset().x as f32) as f64,
None => node.scroll_offset().x,
})
.unwrap_or(0.0);
Ok(JsValue::from(value))
}
fn set_scroll_left(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
set_scroll_axis(this, args, context, true)
}
fn get_scroll_top(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
ctx.flush_layout();
let node_id = this_node_id(this)?;
let value = ctx
.doc
.borrow()
.get_node(node_id)
.map(|node| match node.primary_styles() {
Some(styles) => styles.effective_zoom.unzoom(node.scroll_offset().y as f32) as f64,
None => node.scroll_offset().y,
})
.unwrap_or(0.0);
Ok(JsValue::from(value))
}
fn set_scroll_top(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
set_scroll_axis(this, args, context, false)
}
fn set_scroll_axis(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
horizontal: bool,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
ctx.flush_layout();
let node_id = this_node_id(this)?;
let requested = args
.first()
.unwrap_or(&JsValue::undefined())
.to_number(context)?;
let target_css = if requested.is_finite() {
requested.max(0.0)
} else {
0.0
};
let mut doc = ctx.doc.borrow_mut();
let target = doc
.get_node(node_id)
.and_then(|node| node.primary_styles())
.map(|styles| f64::from(styles.effective_zoom.zoom(target_css as f32)))
.unwrap_or(target_css);
let current = doc
.get_node(node_id)
.map(|node| {
if horizontal {
node.scroll_offset().x
} else {
node.scroll_offset().y
}
})
.unwrap_or(0.0);
let (delta_x, delta_y) = if horizontal {
(current - target, 0.0)
} else {
(0.0, current - target)
};
if doc.scroll_by(Some(node_id), delta_x, delta_y, &mut |_| {}) {
doc.shell_provider.request_redraw();
}
Ok(JsValue::undefined())
}
fn box_metric(
this: &JsValue,
context: &mut Context,
measure: impl Fn(&blitz_dom::Node) -> f32,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
ctx.flush_layout();
let node_id = this_node_id(this)?;
let doc = ctx.doc.borrow();
let value = doc
.get_node(node_id)
.map(|node| {
let measured = measure(node);
match node.primary_styles() {
Some(styles) => styles.effective_zoom.unzoom(measured),
None => measured,
}
})
.unwrap_or(0.0);
Ok(JsValue::from(f64::from(value)))
}
fn get_scroll_width(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
box_metric(this, context, |node| {
node.final_layout().size.width + node.final_layout().scroll_width()
})
}
fn get_scroll_height(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();
if let Some(node) = doc.get_node(node_id) {
let measured = node
.data
.downcast_element()
.and_then(|el| el.text_input_data())
.filter(|input| input.layout_width.is_some())
.and_then(|input| input.content_height());
if let Some(measured) = measured {
let value = match node.primary_styles() {
Some(styles) => styles.effective_zoom.unzoom(measured),
None => measured,
};
return Ok(JsValue::from(f64::from(value)));
}
}
}
box_metric(this, context, |node| {
node.final_layout().size.height + node.final_layout().scroll_height()
})
}
fn get_client_width(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
box_metric(this, context, |node| {
let layout = node.final_layout();
(layout.size.width - layout.border.left - layout.border.right - layout.scrollbar_size.width)
.max(0.0)
})
}
fn get_client_height(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
box_metric(this, context, |node| {
let layout = node.final_layout();
(layout.size.height
- layout.border.top
- layout.border.bottom
- layout.scrollbar_size.height)
.max(0.0)
})
}
fn get_offset_width(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
box_metric(this, context, |node| node.final_layout().size.width)
}
fn get_offset_height(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
box_metric(this, context, |node| node.final_layout().size.height)
}
fn get_offset_left(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
box_metric(this, context, |node| node.offset_top_left().x)
}
fn get_offset_top(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
box_metric(this, context, |node| node.offset_top_left().y)
}
fn get_bounding_client_rect(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
ctx.flush_layout();
let node_id = this_node_id(this)?;
let rect = ctx.doc.borrow().get_client_bounding_rect(node_id);
let (x, y, width, height) = match rect {
Some(rect) => (rect.x, rect.y, rect.width, rect.height),
None => (0.0, 0.0, 0.0, 0.0),
};
let object = ObjectInitializer::new(context)
.property(js_string!("x"), x, PropAttribute::all())
.property(js_string!("y"), y, PropAttribute::all())
.property(js_string!("width"), width, PropAttribute::all())
.property(js_string!("height"), height, PropAttribute::all())
.property(js_string!("left"), x, PropAttribute::all())
.property(js_string!("top"), y, PropAttribute::all())
.property(js_string!("right"), x + width, PropAttribute::all())
.property(js_string!("bottom"), y + height, PropAttribute::all())
.build();
Ok(object.into())
}
pub(crate) fn invalid_selector(selector: &str, context: &mut Context) -> boa_engine::JsError {
crate::dom_exception::error(
"SyntaxError",
&format!("'{selector}' is not a valid selector"),
context,
)
}
pub(crate) fn query_selector(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let selector = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let result = ctx
.doc
.borrow()
.query_selector_in(node_id, &selector)
.map_err(|_| invalid_selector(&selector, context))?;
Ok(super::node_or_null(&ctx, result, context))
}
pub(crate) fn query_selector_all(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let selector = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let matches = ctx
.doc
.borrow()
.query_selector_all_in(node_id, &selector)
.map_err(|_| invalid_selector(&selector, context))?;
let wrappers: Vec<JsValue> = matches
.into_iter()
.map(|match_id| node_wrapper(&ctx, match_id, context).into())
.collect();
Ok(boa_engine::object::builtins::JsArray::from_iter(wrappers, context).into())
}
fn matches_selector(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let selector = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let is_match = ctx
.doc
.borrow()
.matches_selector(node_id, &selector)
.map_err(|_| invalid_selector(&selector, context))?;
Ok(JsValue::from(is_match))
}
fn closest(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let selector = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let result = ctx
.doc
.borrow()
.closest(node_id, &selector)
.map_err(|_| invalid_selector(&selector, context))?;
Ok(super::node_or_null(&ctx, result, context))
}
#[derive(Clone, Copy)]
enum UrlPart {
Protocol,
Username,
Password,
Host,
Hostname,
Port,
Pathname,
Search,
Hash,
Origin,
}
macro_rules! url_part_accessors {
($($get:ident, $set:ident, $name:literal, $part:expr;)*) => {
fn init_hyperlink_utils(proto: &JsObject, context: &mut Context) {
$(define_accessor(proto, $name, Some($get), Some($set), context);)*
define_accessor(proto, "origin", Some(get_url_origin), None, context);
}
$(
fn $get(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
url_part_get(this, $part, context)
}
fn $set(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
url_part_set(this, args, $part, context)
}
)*
};
}
url_part_accessors! {
get_url_protocol, set_url_protocol, "protocol", UrlPart::Protocol;
get_url_username, set_url_username, "username", UrlPart::Username;
get_url_password, set_url_password, "password", UrlPart::Password;
get_url_host, set_url_host, "host", UrlPart::Host;
get_url_hostname, set_url_hostname, "hostname", UrlPart::Hostname;
get_url_port, set_url_port, "port", UrlPart::Port;
get_url_pathname, set_url_pathname, "pathname", UrlPart::Pathname;
get_url_search, set_url_search, "search", UrlPart::Search;
get_url_hash, set_url_hash, "hash", UrlPart::Hash;
}
fn get_url_origin(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
url_part_get(this, UrlPart::Origin, context)
}
fn is_hyperlink(ctx: &DomCtx, node_id: NodeId) -> bool {
ctx.doc
.borrow()
.get_node(node_id)
.and_then(|node| node.element_data())
.is_some_and(|element| {
element.name.ns == markup5ever::ns!(html)
&& matches!(&*element.name.local, "a" | "area")
})
}
fn hyperlink_url(ctx: &DomCtx, node_id: NodeId) -> Option<url::Url> {
let href = read_attr(ctx, node_id, "href")?;
ctx.doc.borrow().url().join(&href).ok()
}
fn url_part_get(this: &JsValue, part: UrlPart, context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
if !is_hyperlink(&ctx, node_id) {
return Ok(JsValue::undefined());
}
let Some(url) = hyperlink_url(&ctx, node_id) else {
return Ok(js_str(""));
};
let value = match part {
UrlPart::Protocol => format!("{}:", url.scheme()),
UrlPart::Username => url.username().to_string(),
UrlPart::Password => url.password().unwrap_or_default().to_string(),
UrlPart::Host => match (url.host_str(), url.port()) {
(Some(host), Some(port)) => format!("{host}:{port}"),
(Some(host), None) => host.to_string(),
_ => String::new(),
},
UrlPart::Hostname => url.host_str().unwrap_or_default().to_string(),
UrlPart::Port => url.port().map(|p| p.to_string()).unwrap_or_default(),
UrlPart::Pathname => url.path().to_string(),
UrlPart::Search => url
.query()
.filter(|q| !q.is_empty())
.map(|q| format!("?{q}"))
.unwrap_or_default(),
UrlPart::Hash => url
.fragment()
.filter(|f| !f.is_empty())
.map(|f| format!("#{f}"))
.unwrap_or_default(),
UrlPart::Origin => url.origin().ascii_serialization(),
};
Ok(js_str(&value))
}
fn url_part_set(
this: &JsValue,
args: &[JsValue],
part: UrlPart,
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let node_id = this_node_id(this)?;
let value = to_rust_string(&args.first().cloned().unwrap_or_default(), context)?;
if !is_hyperlink(&ctx, node_id) {
return Ok(JsValue::undefined());
}
let Some(mut url) = hyperlink_url(&ctx, node_id) else {
return Ok(JsValue::undefined());
};
let applied = match part {
UrlPart::Protocol => url.set_scheme(value.trim_end_matches(':')).is_ok(),
UrlPart::Username => url.set_username(&value).is_ok(),
UrlPart::Password => url
.set_password(Some(value.as_str()).filter(|v| !v.is_empty()))
.is_ok(),
UrlPart::Host => match value.rsplit_once(':') {
Some((host, port)) if port.parse::<u16>().is_ok() => {
url.set_host(Some(host)).is_ok() && url.set_port(port.parse().ok()).is_ok()
}
_ => url.set_host(Some(&value)).is_ok(),
},
UrlPart::Hostname => url.set_host(Some(&value)).is_ok(),
UrlPart::Port => url.set_port(value.parse().ok()).is_ok(),
UrlPart::Pathname => {
url.set_path(&value);
true
}
UrlPart::Search => {
let query = value.strip_prefix('?').unwrap_or(&value);
url.set_query(Some(query).filter(|q| !q.is_empty()));
true
}
UrlPart::Hash => {
let fragment = value.strip_prefix('#').unwrap_or(&value);
url.set_fragment(Some(fragment).filter(|f| !f.is_empty()));
true
}
UrlPart::Origin => false,
};
if applied {
write_attr(&ctx, node_id, "href", url.as_str());
}
Ok(JsValue::undefined())
}