From d2383445ff456b508cd615137b4ecc07ac87cdb7 Mon Sep 17 00:00:00 2001
From: luoxiaozero <48741584+luoxiaozero@users.noreply.github.com>
Date: Mon, 4 Mar 2024 22:16:18 +0800
Subject: [PATCH] Refactor/css transition (#128)

* refactor: CSSTransition

* fix: CSSTransition timeout processing

* fix: CSSTransition repeated processing

* fix: Modal css transition
---
 thaw/src/components/css_transition/mod.rs | 280 ++++++++++++++++------
 thaw/src/modal/mod.rs                     |  32 ++-
 thaw/src/modal/modal.css                  |  12 +-
 3 files changed, 239 insertions(+), 85 deletions(-)

diff --git a/thaw/src/components/css_transition/mod.rs b/thaw/src/components/css_transition/mod.rs
index 3951ea4..437021c 100644
--- a/thaw/src/components/css_transition/mod.rs
+++ b/thaw/src/components/css_transition/mod.rs
@@ -1,8 +1,9 @@
+use crate::utils::{add_event_listener, EventListenerHandle};
 use leptos::{html::ElementDescriptor, *};
-use std::ops::Deref;
+use std::{ops::Deref, time::Duration};
 
 /// # CSS Transition
-/// 
+///
 /// Reference to https://vuejs.org/guide/built-ins/transition.html
 #[component]
 pub fn CSSTransition<T, CF, IV>(
@@ -20,91 +21,234 @@ where
     IV: IntoView,
 {
     let display = create_rw_signal((!show.get_untracked()).then_some("display: none;"));
-    let remove_class_name = store_value(None::<RemoveClassName>);
 
     node_ref.on_load(move |node_el| {
-        let el = node_el.clone().into_any();
-        let el = el.deref();
+        let any_el = node_el.clone().into_any();
+        let el = any_el.deref().clone();
         let class_list = el.class_list();
-        let remove_class = Callback::new(move |_| {
-            remove_class_name.update_value(|class| {
-                if let Some(class) = class.take() {
-                    match class {
-                        RemoveClassName::Enter(active, to) => {
-                            let _ = class_list.remove_2(&active, &to);
-                            if let Some(on_after_enter) = on_after_enter {
-                                on_after_enter.call(());
-                            }
-                        }
-                        RemoveClassName::Leave(active, to) => {
-                            let _ = class_list.remove_2(&active, &to);
-                            display.set(Some("display: none;"));
-                            if let Some(on_after_leave) = on_after_leave {
-                                on_after_leave.call(());
-                            }
-                        }
-                    }
+        let end_handle = StoredValue::new(None::<EventListenerHandle>);
+        let end_count = StoredValue::new(None::<usize>);
+        let finish = StoredValue::new(None::<Callback<()>>);
+
+        let on_end = Callback::new(move |remove: Callback<()>| {
+            let Some(CSSTransitionInfo {
+                types,
+                prop_count,
+                timeout,
+            }) = get_transition_info(&el)
+            else {
+                remove.call(());
+                return;
+            };
+
+            finish.set_value(Some(Callback::new(move |_| {
+                end_count.set_value(None);
+                remove.call(());
+                end_handle.update_value(|h| {
+                    h.take().map(|h| {
+                        h.remove();
+                    });
+                });
+            })));
+
+            set_timeout(
+                move || {
+                    finish.update_value(|v| {
+                        v.take().map(|f| f.call(()));
+                    });
+                },
+                Duration::from_millis(timeout + 1),
+            );
+
+            end_count.set_value(Some(0));
+            let event_listener = move || {
+                end_count.update_value(|v| {
+                    let Some(v) = v else {
+                        return;
+                    };
+                    *v += 1;
+                });
+                if end_count.with_value(|v| {
+                    let Some(v) = v else {
+                        return false;
+                    };
+                    *v >= prop_count
+                }) {
+                    finish.update_value(|v| {
+                        v.take().map(|f| f.call(()));
+                    });
                 }
-            });
+            };
+            let handle = match types {
+                AnimationTypes::Transition => {
+                    add_event_listener(any_el.clone(), ev::transitionend, move |_| event_listener())
+                }
+                AnimationTypes::Animation => {
+                    add_event_listener(any_el.clone(), ev::animationend, move |_| event_listener())
+                }
+            };
+            end_handle.set_value(Some(handle));
         });
 
-        let _ = node_el
-            .on(ev::transitionend, move |_| {
-                remove_class.call(());
-            })
-            .on(ev::animationend, move |_| {
-                remove_class.call(());
+        let on_finish = move || {
+            finish.update_value(|v| {
+                v.take().map(|f| f.call(()));
             });
-    });
+        };
 
-    create_render_effect(move |prev: Option<bool>| {
-        let show = show.get();
-        if let Some(node_el) = node_ref.get_untracked() {
-            if let Some(prev) = prev {
-                let name = name.get_untracked();
+        let on_enter_fn = {
+            let class_list = class_list.clone();
+            Callback::new(move |name: String| {
+                let enter_from = format!("{name}-enter-from");
+                let enter_active = format!("{name}-enter-active");
+                let enter_to = format!("{name}-enter-to");
 
-                let el = node_el.into_any();
-                let el = el.deref();
-                let class_list = el.class_list();
+                let _ = class_list.add_2(&enter_from, &enter_active);
+                display.set(None);
 
-                if show && !prev {
-                    let enter_from = format!("{name}-enter-from");
-                    let enter_active = format!("{name}-enter-active");
-                    let enter_to = format!("{name}-enter-to");
+                let class_list = class_list.clone();
+                next_frame(move || {
+                    let _ = class_list.remove_1(&enter_from);
+                    let _ = class_list.add_1(&enter_to);
 
-                    let _ = class_list.add_2(&enter_from, &enter_active);
-                    display.set(None);
-                    request_animation_frame(move || {
-                        let _ = class_list.remove_1(&enter_from);
-                        let _ = class_list.add_1(&enter_to);
-                        remove_class_name
-                            .set_value(Some(RemoveClassName::Enter(enter_active, enter_to)));
-                        if let Some(on_enter) = on_enter {
-                            on_enter.call(());
+                    let remove = Callback::new(move |_| {
+                        let _ = class_list.remove_2(&enter_active, &enter_to);
+                        if let Some(on_after_enter) = on_after_enter {
+                            on_after_enter.call(());
                         }
                     });
-                } else if !show && prev {
-                    let leave_from = format!("{name}-leave-from");
-                    let leave_active = format!("{name}-leave-active");
-                    let leave_to = format!("{name}-leave-to");
+                    on_end.call(remove);
 
-                    let _ = class_list.add_2(&leave_from, &leave_active);
-                    request_animation_frame(move || {
-                        let _ = class_list.remove_1(&leave_from);
-                        let _ = class_list.add_1(&leave_to);
-                        remove_class_name
-                            .set_value(Some(RemoveClassName::Leave(leave_active, leave_to)));
+                    if let Some(on_enter) = on_enter {
+                        on_enter.call(());
+                    }
+                });
+            })
+        };
+
+        let on_leave_fn = {
+            let class_list = class_list.clone();
+            Callback::new(move |name: String| {
+                let leave_from = format!("{name}-leave-from");
+                let leave_active = format!("{name}-leave-active");
+                let leave_to = format!("{name}-leave-to");
+
+                let _ = class_list.add_2(&leave_from, &leave_active);
+
+                let class_list = class_list.clone();
+                next_frame(move || {
+                    let _ = class_list.remove_1(&leave_from);
+                    let _ = class_list.add_1(&leave_to);
+
+                    let remove = Callback::new(move |_| {
+                        let _ = class_list.remove_2(&leave_active, &leave_to);
+                        display.set(Some("display: none;"));
+                        if let Some(on_after_leave) = on_after_leave {
+                            on_after_leave.call(());
+                        }
                     });
-                }
+                    on_end.call(remove);
+                });
+            })
+        };
+
+        create_render_effect(move |prev: Option<bool>| {
+            let show = show.get();
+            let Some(prev) = prev else {
+                return show;
+            };
+
+            let name = name.get_untracked();
+
+            if show && !prev {
+                on_finish();
+                on_enter_fn.call(name);
+            } else if !show && prev {
+                on_finish();
+                on_leave_fn.call(name);
             }
-        }
-        show
+
+            show
+        });
     });
 
     children(display.read_only())
 }
 
-enum RemoveClassName {
-    Enter(String, String),
-    Leave(String, String),
+fn next_frame(cb: impl FnOnce() + 'static) {
+    request_animation_frame(move || {
+        request_animation_frame(cb);
+    });
+}
+
+#[derive(PartialEq)]
+enum AnimationTypes {
+    Transition,
+    Animation,
+}
+
+struct CSSTransitionInfo {
+    types: AnimationTypes,
+    prop_count: usize,
+    timeout: u64,
+}
+
+fn get_transition_info(el: &web_sys::HtmlElement) -> Option<CSSTransitionInfo> {
+    let styles = window().get_computed_style(el).ok().flatten()?;
+
+    let get_style_properties = |property: &str| {
+        styles
+            .get_property_value(property)
+            .unwrap_or_default()
+            .split(", ")
+            .map(|s| s.to_string())
+            .collect::<Vec<_>>()
+    };
+
+    let transition_delays = get_style_properties("transition-delay");
+    let transition_durations = get_style_properties("transition-duration");
+    let transition_timeout = get_timeout(transition_delays, &transition_durations);
+    let animation_delays = get_style_properties("animation-delay");
+    let animation_durations = get_style_properties("animation-duration");
+    let animation_timeout = get_timeout(animation_delays, &animation_durations);
+
+    let timeout = u64::max(transition_timeout, animation_timeout);
+    let (types, prop_count) = if timeout > 0 {
+        if transition_timeout > animation_timeout {
+            (AnimationTypes::Transition, transition_durations.len())
+        } else {
+            (AnimationTypes::Animation, animation_durations.len())
+        }
+    } else {
+        return None;
+    };
+
+    Some(CSSTransitionInfo {
+        types,
+        prop_count,
+        timeout,
+    })
+}
+
+fn get_timeout(mut delays: Vec<String>, durations: &Vec<String>) -> u64 {
+    while delays.len() < durations.len() {
+        delays.append(&mut delays.clone())
+    }
+
+    fn to_ms(s: &String) -> u64 {
+        if s == "auto" || s.is_empty() {
+            return 0;
+        }
+
+        let s = s.split_at(s.len() - 1).0;
+
+        (s.parse::<f32>().unwrap_or_default() * 1000.0).floor() as u64
+    }
+
+    durations
+        .iter()
+        .enumerate()
+        .map(|(i, d)| to_ms(d) + to_ms(&delays[i]))
+        .max()
+        .unwrap_or_default()
 }
diff --git a/thaw/src/modal/mod.rs b/thaw/src/modal/mod.rs
index 67c3030..837e38b 100644
--- a/thaw/src/modal/mod.rs
+++ b/thaw/src/modal/mod.rs
@@ -22,6 +22,15 @@ pub fn Modal(
 ) -> impl IntoView {
     mount_style("modal", include_str!("./modal.css"));
 
+    let displayed = RwSignal::new(show.get_untracked());
+    Effect::new(move |prev| {
+        let show = show.get();
+        if prev.is_some() && show {
+            displayed.set(true);
+        }
+        show
+    });
+
     let on_mask_click = move |_| {
         if mask_closeable.get_untracked() {
             show.set(false);
@@ -73,15 +82,16 @@ pub fn Modal(
                         ref=mask_ref
                     ></div>
                 </CSSTransition>
-                <CSSTransition
-                    node_ref=scroll_ref
-                    show=show.signal()
-                    name="fade-in-scale-up-transition"
-                    on_enter
-                    let:display
-                >
-                    <div class="thaw-modal-scroll" style=move || display.get() ref=scroll_ref>
-                        <div class="thaw-modal-body" ref=modal_ref role="dialog" aria-modal="true">
+                <div class="thaw-modal-scroll" style=move || (!displayed.get()).then_some("display: none") ref=scroll_ref>
+                    <CSSTransition
+                        node_ref=modal_ref
+                        show=show.signal()
+                        name="fade-in-scale-up-transition"
+                        on_enter
+                        on_after_leave=move |_| displayed.set(false)
+                        let:display
+                    >
+                        <div class="thaw-modal-body" ref=modal_ref role="dialog" aria-modal="true" style=move || display.get()>
                             <Card>
                                 <CardHeader slot>
                                     <span class="thaw-model-title">{move || title.get()}</span>
@@ -102,8 +112,8 @@ pub fn Modal(
                                 </CardFooter>
                             </Card>
                         </div>
-                    </div>
-                </CSSTransition>
+                    </CSSTransition>
+                </div>
             </div>
         </Teleport>
     }
diff --git a/thaw/src/modal/modal.css b/thaw/src/modal/modal.css
index 6029ec3..97efb78 100644
--- a/thaw/src/modal/modal.css
+++ b/thaw/src/modal/modal.css
@@ -57,26 +57,26 @@
     font-size: 16px;
 }
 
-.fade-in-scale-up-transition-leave-active > .thaw-modal-body {
+.fade-in-scale-up-transition-leave-active.thaw-modal-body {
     transform-origin: inherit;
     transition: opacity 0.25s cubic-bezier(0.4, 0, 1, 1),
         transform 0.25s cubic-bezier(0.4, 0, 1, 1);
 }
 
-.fade-in-scale-up-transition-enter-active > .thaw-modal-body {
+.fade-in-scale-up-transition-enter-active.thaw-modal-body {
     transform-origin: inherit;
     transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1),
         transform 0.25s cubic-bezier(0, 0, 0.2, 1);
 }
 
-.fade-in-scale-up-transition-enter-from > .thaw-modal-body,
-.fade-in-scale-up-transition-leave-to > .thaw-modal-body {
+.fade-in-scale-up-transition-enter-from.thaw-modal-body,
+.fade-in-scale-up-transition-leave-to.thaw-modal-body {
     opacity: 0;
     transform: scale(0.5);
 }
 
-.fade-in-scale-up-transition-leave-from > .thaw-modal-body,
-.fade-in-scale-up-transition-enter-to > .thaw-modal-body {
+.fade-in-scale-up-transition-leave-from.thaw-modal-body,
+.fade-in-scale-up-transition-enter-to.thaw-modal-body {
     opacity: 1;
     transform: scale(1);
 }