use std::borrow::Cow;
use std::cell::RefCell;
use std::rc::Rc;
use js_sys::{Object, Reflect};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use web_sys::Element;
use crate::expr::{self, Spanned};
use crate::mount::track_effect_on;
use crate::reactive::effect;
use crate::scope::with_current_el;
fn normalize_prop_name(name: &str) -> String {
name.replace('-', "_")
}
pub fn install(el: &Element, parent_proxy: &JsValue, attr: &str, ast: Spanned<expr::Expr>) {
install_eval(
el,
parent_proxy,
attr,
Rc::new(move |scope| expr::evaluate(&ast, scope)),
);
}
#[doc(hidden)]
pub fn install_eval(
el: &Element,
parent_proxy: &JsValue,
attr: &str,
evaluator: Rc<dyn Fn(&JsValue) -> JsValue>,
) {
let el_owned = el.clone();
let parent_proxy_owned = parent_proxy.clone();
let attr_owned = attr.to_string();
let child_target = crate::mount::child_component_scope(el);
let child_field = normalize_prop_name(attr);
let prev: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
let id = effect(move || {
with_current_el(&el_owned.clone(), || {
let v = evaluator(&parent_proxy_owned);
match &child_target {
Some((child_scope_id, cp)) => {
let target_field =
crate::model_runtime::resolve_model_key(*child_scope_id, &child_field)
.unwrap_or_else(|| child_field.clone());
let is_prop = crate::scope::Scope::find(*child_scope_id)
.map(|s| s.state.borrow().is_prop(&target_field))
.unwrap_or(false);
if !is_prop {
return;
}
crate::model_runtime::with_write_origin(
crate::model_runtime::WriteOrigin::ParentModelIn,
|| {
let _ = Reflect::set(cp, &JsValue::from_str(&target_field), &v);
},
);
}
None => apply_memoised(&el_owned, &attr_owned, &v, &prev),
}
});
});
track_effect_on(el, id);
}
fn apply_memoised(el: &Element, attr: &str, v: &JsValue, prev: &Rc<RefCell<Option<String>>>) {
let dom_attr = dom_attr_name(el, attr);
let dom_attr = dom_attr.as_ref();
let should_remove = v.is_undefined() || v.is_null() || v == &JsValue::FALSE;
if should_remove {
let mut p = prev.borrow_mut();
if p.is_none() {
return;
}
*p = None;
let _ = el.remove_attribute(dom_attr);
if dom_attr != attr {
let _ = el.remove_attribute(attr);
}
return;
}
let serialised: String = match attr {
"class" => match serialise_class(v) {
Some(s) => s,
None => return, },
"style" => match serialise_style(v) {
Some(s) => s,
None => return,
},
_ => match serialise_plain(attr, v) {
Some(s) => s,
None => return,
},
};
{
let p = prev.borrow();
if p.as_deref() == Some(serialised.as_str()) {
return;
}
}
if dom_attr != attr {
let _ = el.remove_attribute(attr);
}
let _ = el.set_attribute(dom_attr, &serialised);
*prev.borrow_mut() = Some(serialised);
}
const SVG_NS: &str = "http://www.w3.org/2000/svg";
fn dom_attr_name<'a>(el: &Element, attr: &'a str) -> Cow<'a, str> {
if el.namespace_uri().as_deref() != Some(SVG_NS) {
return Cow::Borrowed(attr);
}
canonical_svg_attr(attr)
.map(Cow::Borrowed)
.unwrap_or(Cow::Borrowed(attr))
}
fn canonical_svg_attr(attr: &str) -> Option<&'static str> {
match attr {
"attributename" => Some("attributeName"),
"attributetype" => Some("attributeType"),
"basefrequency" => Some("baseFrequency"),
"baseprofile" => Some("baseProfile"),
"calcmode" => Some("calcMode"),
"clippathunits" => Some("clipPathUnits"),
"diffuseconstant" => Some("diffuseConstant"),
"edgemode" => Some("edgeMode"),
"filterunits" => Some("filterUnits"),
"glyphref" => Some("glyphRef"),
"gradienttransform" => Some("gradientTransform"),
"gradientunits" => Some("gradientUnits"),
"kernelmatrix" => Some("kernelMatrix"),
"kernelunitlength" => Some("kernelUnitLength"),
"keypoints" => Some("keyPoints"),
"keysplines" => Some("keySplines"),
"keytimes" => Some("keyTimes"),
"lengthadjust" => Some("lengthAdjust"),
"limitingconeangle" => Some("limitingConeAngle"),
"markerheight" => Some("markerHeight"),
"markerunits" => Some("markerUnits"),
"markerwidth" => Some("markerWidth"),
"maskcontentunits" => Some("maskContentUnits"),
"maskunits" => Some("maskUnits"),
"numoctaves" => Some("numOctaves"),
"pathlength" => Some("pathLength"),
"patterncontentunits" => Some("patternContentUnits"),
"patterntransform" => Some("patternTransform"),
"patternunits" => Some("patternUnits"),
"pointsatx" => Some("pointsAtX"),
"pointsaty" => Some("pointsAtY"),
"pointsatz" => Some("pointsAtZ"),
"preservealpha" => Some("preserveAlpha"),
"preserveaspectratio" => Some("preserveAspectRatio"),
"primitiveunits" => Some("primitiveUnits"),
"refx" => Some("refX"),
"refy" => Some("refY"),
"repeatcount" => Some("repeatCount"),
"repeatdur" => Some("repeatDur"),
"requiredextensions" => Some("requiredExtensions"),
"requiredfeatures" => Some("requiredFeatures"),
"specularconstant" => Some("specularConstant"),
"specularexponent" => Some("specularExponent"),
"spreadmethod" => Some("spreadMethod"),
"startoffset" => Some("startOffset"),
"stddeviation" => Some("stdDeviation"),
"surfacescale" => Some("surfaceScale"),
"systemlanguage" => Some("systemLanguage"),
"tablevalues" => Some("tableValues"),
"targetx" => Some("targetX"),
"targety" => Some("targetY"),
"textlength" => Some("textLength"),
"viewbox" => Some("viewBox"),
"viewtarget" => Some("viewTarget"),
"xchannelselector" => Some("xChannelSelector"),
"ychannelselector" => Some("yChannelSelector"),
"zoomandpan" => Some("zoomAndPan"),
_ => None,
}
}
fn is_state_attr(attr: &str) -> bool {
attr.starts_with("data-") || attr.starts_with("aria-")
}
fn serialise_class(v: &JsValue) -> Option<String> {
if let Some(s) = v.as_string() {
return Some(s);
}
if v.is_object() {
let obj: Object = v.clone().unchecked_into();
let keys = Object::keys(&obj);
let mut out: Vec<String> = Vec::new();
for i in 0..keys.length() {
let k = keys.get(i);
let truthy = Reflect::get(&obj, &k)
.map(|val| val.as_bool().unwrap_or(!val.is_falsy()))
.unwrap_or(false);
if truthy {
if let Some(s) = k.as_string() {
out.push(s);
}
}
}
return Some(out.join(" "));
}
None
}
fn serialise_style(v: &JsValue) -> Option<String> {
if let Some(s) = v.as_string() {
return Some(s);
}
if v.is_object() {
let obj: Object = v.clone().unchecked_into();
let keys = Object::keys(&obj);
let mut out = String::new();
for i in 0..keys.length() {
let k = keys.get(i);
if let (Some(name), Ok(val)) = (k.as_string(), Reflect::get(&obj, &k)) {
let val_s = val.as_string().unwrap_or_default();
out.push_str(&format!("{name}:{val_s};"));
}
}
return Some(out);
}
None
}
fn serialise_plain(attr: &str, v: &JsValue) -> Option<String> {
if let Some(s) = v.as_string() {
return Some(s);
}
if let Some(n) = v.as_f64() {
return Some(n.to_string());
}
if let Some(b) = v.as_bool() {
if is_state_attr(attr) {
return Some(if b { "true".into() } else { "false".into() });
}
return Some(String::new());
}
Some(
js_sys::JSON::stringify(v)
.ok()
.and_then(|s| s.as_string())
.unwrap_or_default(),
)
}