use super::*;
unsafe impl Sync for DomOpTableCell {}
pub(crate) fn ensure_dom_op_table() -> Option<DomOpTable> {
DOM_OP_TABLE_CELL.with(|cell: &DomOpTableCell| {
let cached_ptr: *mut Option<DomOpTable> = cell.0.get();
unsafe {
if let Some(table) = &*cached_ptr {
return Some(table.clone());
}
}
let global_value: JsValue = global_this()?;
let table_value: JsValue =
match Reflect::get(&global_value, &JsValue::from_str(JS_DOM_OP_TABLE)) {
Ok(existing) => existing,
Err(_err) => JsValue::UNDEFINED,
};
let table: DomOpTable = if table_value.is_object() {
let set_attrs: Function =
match Reflect::get(&table_value, &JsValue::from_str(JS_DOM_OP_SET_ATTRS)) {
Ok(value) => match value.dyn_into::<Function>() {
Ok(function) => function,
Err(_) => return None,
},
Err(_err) => return None,
};
let remove_attrs: Function =
match Reflect::get(&table_value, &JsValue::from_str(JS_DOM_OP_REMOVE_ATTRS)) {
Ok(value) => match value.dyn_into::<Function>() {
Ok(function) => function,
Err(_) => return None,
},
Err(_err) => return None,
};
let child_ops: Function =
match Reflect::get(&table_value, &JsValue::from_str(JS_DOM_OP_CHILD_OPS)) {
Ok(value) => match value.dyn_into::<Function>() {
Ok(function) => function,
Err(_) => return None,
},
Err(_err) => return None,
};
DomOpTable {
set_attrs,
remove_attrs,
child_ops,
}
} else {
install_dom_op_table(&global_value)?
};
unsafe {
*cached_ptr = Some(table.clone());
}
Some(table)
})
}
fn install_dom_op_table(global_value: &JsValue) -> Option<DomOpTable> {
let set_attrs_source: &str = "function(elem, names, values) { \
for (var i = 0; i < names.length; i++) { \
elem.setAttribute(names[i], values[i]); \
} \
}";
let remove_attrs_source: &str = "function(elem, names) { \
for (var i = 0; i < names.length; i++) { \
elem.removeAttribute(names[i]); \
} \
}";
let child_ops_source: &str = "function(parent, ops) { \
for (var i = 0; i < ops.length; i++) { \
var op = ops[i]; \
if (op[0] === 0) { \
parent.insertBefore(op[1], op[2]); \
} else if (op[0] === 1) { \
parent.appendChild(op[1]); \
} else { \
parent.removeChild(op[1]); \
} \
} \
}";
let set_attrs: Function = eval_function(set_attrs_source)?;
let remove_attrs: Function = eval_function(remove_attrs_source)?;
let child_ops: Function = eval_function(child_ops_source)?;
let table_value: JsValue = js_sys::Object::new().into();
let _ = Reflect::set(
&table_value,
&JsValue::from_str(JS_DOM_OP_SET_ATTRS),
set_attrs.as_ref(),
);
let _ = Reflect::set(
&table_value,
&JsValue::from_str(JS_DOM_OP_REMOVE_ATTRS),
remove_attrs.as_ref(),
);
let _ = Reflect::set(
&table_value,
&JsValue::from_str(JS_DOM_OP_CHILD_OPS),
child_ops.as_ref(),
);
let _ = Reflect::set(
global_value,
&JsValue::from_str(JS_DOM_OP_TABLE),
&table_value,
);
Some(DomOpTable {
set_attrs,
remove_attrs,
child_ops,
})
}
fn eval_function(body: &str) -> Option<Function> {
let wrapped: String = format!("({})", body);
js_sys::eval(&wrapped)
.ok()
.and_then(|value: JsValue| value.dyn_into::<Function>().ok())
}
fn global_this() -> Option<JsValue> {
if let Ok(value) = js_sys::eval("globalThis") {
if !value.is_undefined() {
return Some(value);
}
}
let window: Window = window()?;
Some(window.into())
}
pub(crate) fn is_property_attr(name: &str) -> bool {
name == ATTR_VALUE
|| name == ATTR_CHECKED
|| name == ATTR_DISABLED
|| name == ATTR_SELECTED
|| name == ATTR_READONLY
|| name == ATTR_MULTIPLE
}
pub(crate) fn apply_set_attr_batch(element: &Element, ops: &[(String, String)]) {
if ops.is_empty() {
return;
}
let Some(table) = ensure_dom_op_table() else {
for (name, value) in ops {
let _: Result<(), JsValue> = element.set_attribute(name, value);
}
return;
};
let names: js_sys::Array = js_sys::Array::new_with_length(ops.len() as u32);
let values: js_sys::Array = js_sys::Array::new_with_length(ops.len() as u32);
for (index, (name, value)) in ops.iter().enumerate() {
let _ = names.set(index as u32, JsValue::from_str(name));
let _ = values.set(index as u32, JsValue::from_str(value));
}
let element_value: JsValue = element.clone().into();
let result: Result<JsValue, JsValue> = table.set_attrs.call3(
&JsValue::UNDEFINED,
&element_value,
names.as_ref(),
values.as_ref(),
);
if result.is_err() {
for (name, value) in ops {
let _: Result<(), JsValue> = element.set_attribute(name, value);
}
}
}
pub(crate) fn apply_remove_attr_batch(element: &Element, ops: &[String]) {
if ops.is_empty() {
return;
}
let Some(table) = ensure_dom_op_table() else {
for name in ops {
let _: Result<(), JsValue> = element.remove_attribute(name);
}
return;
};
let names: js_sys::Array = js_sys::Array::new_with_length(ops.len() as u32);
for (index, name) in ops.iter().enumerate() {
let _ = names.set(index as u32, JsValue::from_str(name));
}
let element_value: JsValue = element.clone().into();
let result: Result<JsValue, JsValue> =
table
.remove_attrs
.call2(&JsValue::UNDEFINED, &element_value, names.as_ref());
if result.is_err() {
for name in ops {
let _: Result<(), JsValue> = element.remove_attribute(name);
}
}
}
pub(crate) fn apply_child_ops_batch(parent: &Element, ops: &[ChildOp]) {
if ops.is_empty() {
return;
}
let Some(table) = ensure_dom_op_table() else {
for op in ops {
apply_child_op_fallback(parent, op);
}
return;
};
let ops_array: js_sys::Array = js_sys::Array::new_with_length(ops.len() as u32);
for (index, op) in ops.iter().enumerate() {
let tuple: js_sys::Array = js_sys::Array::new_with_length(3);
let kind: u32 = match op {
ChildOp::InsertBefore { .. } => 0,
ChildOp::AppendChild(_) => 1,
ChildOp::RemoveChild(_) => 2,
};
let primary: JsValue = match op {
ChildOp::InsertBefore { node, .. } => node.clone().into(),
ChildOp::AppendChild(node) => node.clone().into(),
ChildOp::RemoveChild(node) => node.clone().into(),
};
let reference: JsValue = match op {
ChildOp::InsertBefore { reference, .. } => {
reference.clone().map(Node::into).unwrap_or(JsValue::NULL)
}
_ => JsValue::NULL,
};
let _ = tuple.set(0, JsValue::from(kind));
let _ = tuple.set(1, primary);
let _ = tuple.set(2, reference);
let _ = ops_array.set(index as u32, tuple.into());
}
let parent_value: JsValue = parent.clone().into();
let result: Result<JsValue, JsValue> =
table
.child_ops
.call2(&JsValue::UNDEFINED, &parent_value, ops_array.as_ref());
if result.is_err() {
for op in ops {
apply_child_op_fallback(parent, op);
}
}
}
fn apply_child_op_fallback(parent: &Element, op: &ChildOp) {
match op {
ChildOp::InsertBefore { node, reference } => {
let _: Result<Node, JsValue> = match reference {
Some(reference_node) => parent.insert_before(node, Some(reference_node)),
None => parent.append_child(node),
};
}
ChildOp::AppendChild(node) => {
let _: Result<Node, JsValue> = parent.append_child(node);
}
ChildOp::RemoveChild(node) => {
let _: Result<Node, JsValue> = parent.remove_child(node);
}
}
}