use js_sys::Reflect;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use web_sys::{console, Element, HtmlTemplateElement, Node};
use crate::mount::{self, bind_borrowed_scope_to};
const TELEPORTED_KEY: &str = "__pp_teleported";
pub const TELEPORT_ORIGIN_KEY: &str = "__pp_teleport_origin";
pub fn host_of(el: &Element) -> Option<Element> {
let origin_key = JsValue::from_str(TELEPORT_ORIGIN_KEY);
let mut cur: Option<Element> = Some(el.clone());
while let Some(node) = cur {
if let Ok(v) = Reflect::get(node.as_ref(), &origin_key) {
if !v.is_undefined() && !v.is_null() {
if let Ok(template) = v.dyn_into::<Element>() {
return template.parent_element();
}
}
}
cur = node.parent_element();
}
None
}
pub fn install(
template: HtmlTemplateElement,
selector: &str,
body_fn: Option<crate::directives::for_plan::TeleportBodyFn>,
) {
let template_el: Element = template.clone().into();
let Some(target) = resolve_target(selector) else {
console::error_1(&JsValue::from_str(&format!(
"pp-teleport: target selector {selector:?} did not match any element"
)));
return;
};
let pinned_scope = mount::enclosing_scope(&template_el);
let inject_parent_id_override = mount::inherited_ctx_parent_of(&template_el);
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-teleport: 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-teleport: 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-teleport: <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);
}
if target.append_child(clone_root.as_ref()).is_ok() {
let _ = Reflect::set(
clone_root.as_ref(),
&TELEPORT_ORIGIN_KEY.into(),
template_el.as_ref(),
);
if fragment_built {
mount::finalize_compiled_subtree(&clone_root);
} else {
crate::templates_plan::record_plan_failure();
}
stash_teleported(&template_el, &clone_root);
}
}
pub fn resolve_target(selector: &str) -> Option<Element> {
let sel = selector.trim();
let doc = web_sys::window()?.document()?;
if sel == "body" {
return doc.body().map(Element::from);
}
doc.query_selector(sel).ok().flatten()
}
pub fn release(el: &Element) {
let Some(clone) = take_teleported(el) else {
return;
};
if let Some(parent) = clone.parent_node() {
let _ = parent.remove_child(&clone);
}
mount::release_subtree(clone.as_ref());
}
fn stash_teleported(template: &Element, clone: &Element) {
let _ = Reflect::set(template.as_ref(), &TELEPORTED_KEY.into(), clone.as_ref());
}
pub(crate) fn stash(template: &Element, clone: &Element) {
stash_teleported(template, clone);
}
pub(crate) fn clear_stash(template: &Element) {
let _ = Reflect::set(
template.as_ref(),
&TELEPORTED_KEY.into(),
&JsValue::UNDEFINED,
);
}
fn take_teleported(template: &Element) -> Option<Element> {
let v = Reflect::get(template.as_ref(), &TELEPORTED_KEY.into()).ok()?;
if v.is_undefined() || v.is_null() {
return None;
}
let _ = Reflect::set(
template.as_ref(),
&TELEPORTED_KEY.into(),
&JsValue::UNDEFINED,
);
v.dyn_into::<Element>().ok()
}
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
}