use crate::{
animation::{Animation, AnimationState},
use_is_back_navigation, use_route, SetIsRouting,
};
use leptos::{leptos_dom::HydrationCtx, *};
use std::{cell::Cell, rc::Rc};
use web_sys::AnimationEvent;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all,)
)]
#[component]
pub fn Outlet(cx: Scope) -> impl IntoView {
let id = HydrationCtx::id();
let route = use_route(cx);
let is_showing = Rc::new(Cell::new(None::<(usize, Scope)>));
let (outlet, set_outlet) = create_signal(cx, None::<View>);
create_isomorphic_effect(cx, move |_| {
match (route.child(cx), &is_showing.get()) {
(None, prev) => {
if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
prev_scope.dispose();
}
set_outlet.set(None);
}
(Some(child), Some((is_showing_val, _)))
if child.id() == *is_showing_val =>
{
}
(Some(child), prev) => {
if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
prev_scope.dispose();
}
_ = cx.child_scope(|child_cx| {
provide_context(child_cx, child.clone());
set_outlet
.set(Some(child.outlet(child_cx).into_view(child_cx)));
is_showing.set(Some((child.id(), child_cx)));
});
}
}
});
let outlet: Signal<Option<View>> =
if cfg!(any(feature = "csr", feature = "hydrate"))
&& use_context::<SetIsRouting>(cx).is_some()
{
let global_suspense = expect_context::<GlobalSuspenseContext>(cx);
let (current_view, set_current_view) = create_signal(cx, None);
create_effect(cx, {
move |prev| {
let outlet = outlet.get();
let is_fallback =
!global_suspense.with_inner(SuspenseContext::ready);
if prev.is_none() {
set_current_view.set(outlet);
} else if !is_fallback {
queue_microtask({
let global_suspense = global_suspense.clone();
move || {
let is_fallback = cx.untrack(move || {
!global_suspense
.with_inner(SuspenseContext::ready)
});
if !is_fallback {
set_current_view.set(outlet);
}
}
});
}
}
});
current_view.into()
} else {
outlet.into()
};
leptos::leptos_dom::DynChild::new_with_id(id, move || outlet.get())
}
#[component]
pub fn AnimatedOutlet(
cx: Scope,
#[prop(optional, into)]
class: Option<TextProp>,
#[prop(optional)]
outro: Option<&'static str>,
#[prop(optional)]
outro_back: Option<&'static str>,
#[prop(optional)]
start: Option<&'static str>,
#[prop(optional)]
intro: Option<&'static str>,
#[prop(optional)]
intro_back: Option<&'static str>,
#[prop(optional)]
finally: Option<&'static str>,
) -> impl IntoView {
let route = use_route(cx);
let is_showing = Rc::new(Cell::new(None::<(usize, Scope)>));
let (outlet, set_outlet) = create_signal(cx, None::<View>);
let animation = Animation {
outro,
start,
intro,
finally,
outro_back,
intro_back,
};
let (animation_state, set_animation_state) =
create_signal(cx, AnimationState::Finally);
let trigger_animation = create_rw_signal(cx, ());
let is_back = use_is_back_navigation(cx);
let animation_and_outlet = create_memo(cx, {
move |prev: Option<&(AnimationState, View)>| {
let animation_state = animation_state.get();
let next_outlet = outlet.get().unwrap_or_default();
trigger_animation.track();
match prev {
None => (animation_state, next_outlet),
Some((prev_state, prev_outlet)) => {
let (next_state, can_advance) = animation
.next_state(prev_state, is_back.get_untracked());
if can_advance {
(next_state, next_outlet)
} else {
(next_state, prev_outlet.to_owned())
}
}
}
}
});
let current_animation =
create_memo(cx, move |_| animation_and_outlet.get().0);
let current_outlet = create_memo(cx, move |_| animation_and_outlet.get().1);
create_isomorphic_effect(cx, move |_| {
match (route.child(cx), &is_showing.get()) {
(None, prev) => {
if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
prev_scope.dispose();
}
set_outlet.set(None);
}
(Some(child), Some((is_showing_val, _)))
if child.id() == *is_showing_val =>
{
trigger_animation.set(());
}
(Some(child), prev) => {
if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
prev_scope.dispose();
}
_ = cx.child_scope(|child_cx| {
provide_context(child_cx, child.clone());
set_outlet
.set(Some(child.outlet(child_cx).into_view(child_cx)));
is_showing.set(Some((child.id(), child_cx)));
});
}
}
});
let class = move || {
let animation_class = match current_animation.get() {
AnimationState::Outro => outro.unwrap_or_default(),
AnimationState::Start => start.unwrap_or_default(),
AnimationState::Intro => intro.unwrap_or_default(),
AnimationState::Finally => finally.unwrap_or_default(),
AnimationState::OutroBack => outro_back.unwrap_or_default(),
AnimationState::IntroBack => intro_back.unwrap_or_default(),
};
if let Some(class) = &class {
format!("{} {animation_class}", class.get())
} else {
animation_class.to_string()
}
};
let node_ref = create_node_ref::<html::Div>(cx);
let animationend = move |ev: AnimationEvent| {
use wasm_bindgen::JsCast;
if let Some(target) = ev.target() {
let node_ref = node_ref.get();
if node_ref.is_none()
|| target
.unchecked_ref::<web_sys::Node>()
.is_same_node(Some(&*node_ref.unwrap()))
{
ev.stop_propagation();
let current = current_animation.get();
set_animation_state.update(|current_state| {
let (next, _) =
animation.next_state(¤t, is_back.get_untracked());
*current_state = next;
});
}
}
};
view! { cx,
<div class=class on:animationend=animationend>
{move || current_outlet.get()}
</div>
}
}