use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use web_sys::{console, Element, HtmlTemplateElement, Node};
use super::teleport;
use super::transition;
use crate::expr::{self, Spanned};
use crate::mount::{self, bind_borrowed_scope_to, track_effect_on};
use crate::reactive::effect;
pub fn install(
template: HtmlTemplateElement,
parent_proxy: JsValue,
ast: Spanned<expr::Expr>,
body_fn: Option<crate::directives::for_plan::IfBodyFn>,
teleport_selector: Option<&str>,
) {
install_eval(
template,
parent_proxy,
Rc::new(move |scope| expr::evaluate(&ast, scope)),
body_fn,
teleport_selector,
);
}
#[doc(hidden)]
pub fn install_eval(
template: HtmlTemplateElement,
parent_proxy: JsValue,
evaluator: Rc<dyn Fn(&JsValue) -> JsValue>,
body_fn: Option<crate::directives::for_plan::IfBodyFn>,
teleport_selector: Option<&str>,
) {
let template_el: Element = template.clone().into();
let track_anchor = template_el.clone();
let teleport_selector = teleport_selector
.map(str::to_string)
.or_else(|| template_el.get_attribute("pp-teleport"));
let teleport_target: Option<Element> = teleport_selector
.as_deref()
.and_then(teleport::resolve_target);
let pinned_scope = mount::enclosing_scope(&template_el);
let inject_parent_id_override = mount::inherited_ctx_parent_of(&template_el);
let current: Rc<RefCell<Option<Element>>> = Rc::new(RefCell::new(None));
let effect_id = effect(move || {
let truthy = !evaluator(&parent_proxy).is_falsy();
let existing = current.borrow().clone();
match (truthy, existing) {
(true, None) => {
let (clone_root, fragment_built) = match body_fn {
Some(f) => {
let Some((scope_id, scope_proxy)) = pinned_scope.as_ref() else {
console::error_1(&JsValue::from_str(
"pp-if: body fragment requires an enclosing scope",
));
return;
};
let ctx_parent_id = inject_parent_id_override.unwrap_or(*scope_id);
match f(*scope_id, scope_proxy, ctx_parent_id) {
Some(root) => (root, true),
None => {
console::error_1(&JsValue::from_str(
"pp-if: body fragment failed to materialise root",
));
return;
}
}
}
None => match clone_template_body(&template) {
Some(root) => (root, false),
None => {
console::error_1(&JsValue::from_str(
"pp-if: <template> body must contain exactly one element",
));
return;
}
},
};
if let Some((scope_id, proxy)) = pinned_scope.as_ref() {
bind_borrowed_scope_to(&clone_root, *scope_id, proxy);
}
let inserted = match teleport_target.as_ref() {
Some(target) => target.append_child(clone_root.as_ref()).is_ok(),
None => template_el
.parent_node()
.map(|p| {
p.insert_before(clone_root.as_ref(), Some(template_el.as_ref()))
.is_ok()
})
.unwrap_or(false),
};
if inserted {
if teleport_target.is_some() {
let _ = js_sys::Reflect::set(
clone_root.as_ref(),
&teleport::TELEPORT_ORIGIN_KEY.into(),
template_el.as_ref(),
);
teleport::stash(&template_el, &clone_root);
}
if fragment_built {
mount::finalize_compiled_subtree(&clone_root);
} else {
crate::templates_plan::record_plan_failure();
}
*current.borrow_mut() = Some(clone_root.clone());
transition::enter_subtree(&clone_root, || {});
}
}
(true, Some(clone)) => {
if transition::is_subtree_leaving(&clone) {
transition::enter_subtree(&clone, || {});
}
}
(false, Some(clone)) => {
if transition::is_subtree_leaving(&clone) {
return;
}
let clone_cap = clone.clone();
let slot_cap = current.clone();
let template_cap = template_el.clone();
let teleported = teleport_target.is_some();
transition::leave_subtree(&clone, move || {
if let Some(parent) = clone_cap.parent_node() {
let _ = parent.remove_child(&clone_cap);
}
if teleported {
teleport::clear_stash(&template_cap);
}
mount::release_subtree(clone_cap.as_ref());
let mut slot = slot_cap.borrow_mut();
if let Some(cur) = slot.as_ref() {
if cur.is_same_node(Some(clone_cap.as_ref())) {
*slot = None;
}
}
});
}
(false, None) => {}
}
});
track_effect_on(&track_anchor, effect_id);
}
fn clone_template_body(template: &HtmlTemplateElement) -> Option<Element> {
let fragment: Node = template.content().clone_node_with_deep(true).ok()?;
let children = fragment.child_nodes();
for i in 0..children.length() {
if let Some(n) = children.item(i) {
if let Ok(el) = n.dyn_into::<Element>() {
return Some(el);
}
}
}
None
}