From 74eabc34f7982df82fde365a43afd198ffe3fbe0 Mon Sep 17 00:00:00 2001 From: luoxiao Date: Sun, 3 Mar 2024 22:17:11 +0800 Subject: [PATCH 1/4] refactor: CSSTransition --- thaw/src/components/css_transition/mod.rs | 176 +++++++++++++--------- 1 file changed, 102 insertions(+), 74 deletions(-) diff --git a/thaw/src/components/css_transition/mod.rs b/thaw/src/components/css_transition/mod.rs index 3951ea4a..eccc60bc 100644 --- a/thaw/src/components/css_transition/mod.rs +++ b/thaw/src/components/css_transition/mod.rs @@ -1,8 +1,10 @@ use leptos::{html::ElementDescriptor, *}; use std::ops::Deref; +use crate::utils::{add_event_listener, EventListenerHandle}; + /// # CSS Transition -/// +/// /// Reference to https://vuejs.org/guide/built-ins/transition.html #[component] pub fn CSSTransition( @@ -20,91 +22,117 @@ where IV: IntoView, { let display = create_rw_signal((!show.get_untracked()).then_some("display: none;")); - let remove_class_name = store_value(None::); 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(); 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 handle = StoredValue::new(None::<(EventListenerHandle, EventListenerHandle)>); + + let on_end = Callback::new(move |remove: Callback<()>| { + let transition_handle = + add_event_listener(any_el.clone(), ev::transitionend, move |_| { + remove.call(()); + handle.update_value(|h| { + h.take().map(|(t, a)| { + t.remove(); + a.remove(); + }); + }); + }); + let animation_handle = + add_event_listener(any_el.clone(), ev::animationend, move |_| { + remove.call(()); + handle.update_value(|h| { + h.take().map(|(t, a)| { + t.remove(); + a.remove(); + }); + }); + }); + handle.set_value(Some((transition_handle, animation_handle))); }); - let _ = node_el - .on(ev::transitionend, move |_| { - remove_class.call(()); - }) - .on(ev::animationend, move |_| { - remove_class.call(()); - }); - }); + 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 _ = class_list.add_2(&enter_from, &enter_active); + display.set(None); + + let class_list = class_list.clone(); + next_frame(move || { + let _ = class_list.remove_1(&enter_from); + let _ = class_list.add_1(&enter_to); - create_render_effect(move |prev: Option| { - let show = show.get(); - if let Some(node_el) = node_ref.get_untracked() { - if let Some(prev) = prev { - let name = name.get_untracked(); - - let el = node_el.into_any(); - let el = el.deref(); - let class_list = el.class_list(); - - 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.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"); - - 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))); + on_end.call(remove); + + 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| { + let show = show.get(); + let Some(prev) = prev else { + return show; + }; + + let name = name.get_untracked(); + + if show && !prev { + on_enter_fn.call(name); + } else if !show && prev { + on_leave_fn.call(name); } - } - show + + show + }); }); children(display.read_only()) } -enum RemoveClassName { - Enter(String, String), - Leave(String, String), -} + +pub fn next_frame(cb: impl FnOnce() + 'static) { + request_animation_frame(move || { + request_animation_frame(cb); + }); +} \ No newline at end of file From c924ce51d4dca18ec866132832c3e704430d4b6a Mon Sep 17 00:00:00 2001 From: luoxiao Date: Mon, 4 Mar 2024 14:14:04 +0800 Subject: [PATCH 2/4] fix: CSSTransition timeout processing --- thaw/src/components/css_transition/mod.rs | 160 ++++++++++++++++++---- 1 file changed, 134 insertions(+), 26 deletions(-) diff --git a/thaw/src/components/css_transition/mod.rs b/thaw/src/components/css_transition/mod.rs index eccc60bc..ca9a9ef4 100644 --- a/thaw/src/components/css_transition/mod.rs +++ b/thaw/src/components/css_transition/mod.rs @@ -1,7 +1,6 @@ -use leptos::{html::ElementDescriptor, *}; -use std::ops::Deref; - use crate::utils::{add_event_listener, EventListenerHandle}; +use leptos::{html::ElementDescriptor, *}; +use std::{ops::Deref, time::Duration}; /// # CSS Transition /// @@ -25,32 +24,69 @@ where node_ref.on_load(move |node_el| { let any_el = node_el.clone().into_any(); - let el = any_el.deref(); + let el = any_el.deref().clone(); let class_list = el.class_list(); - let handle = StoredValue::new(None::<(EventListenerHandle, EventListenerHandle)>); + let end_handle = StoredValue::new(None::); + let end_count = StoredValue::new(0usize); let on_end = Callback::new(move |remove: Callback<()>| { - let transition_handle = - add_event_listener(any_el.clone(), ev::transitionend, move |_| { - remove.call(()); - handle.update_value(|h| { - h.take().map(|(t, a)| { - t.remove(); - a.remove(); - }); + let Some(CSSTransitionInfo { + types, + prop_count, + timeout, + }) = get_transition_info(&el) + else { + remove.call(()); + return; + }; + let Some(types) = types else { + remove.call(()); + return; + }; + + let remove = move || { + remove.call(()); + end_handle.update_value(|h| { + h.take().map(|h| { + h.remove(); }); }); - let animation_handle = - add_event_listener(any_el.clone(), ev::animationend, move |_| { - remove.call(()); - handle.update_value(|h| { - h.take().map(|(t, a)| { - t.remove(); - a.remove(); + }; + + end_count.set_value(0); + + set_timeout( + move || { + if end_count.with_value(|v| v < &prop_count) { + remove(); + } + }, + Duration::from_millis(timeout + 1), + ); + + let handle = match types { + AnimationTypes::Transition => { + add_event_listener(any_el.clone(), ev::transitionend, move |_| { + end_count.update_value(|v| { + *v += 1; + if *v >= prop_count { + remove(); + } }); - }); - }); - handle.set_value(Some((transition_handle, animation_handle))); + }) + } + AnimationTypes::Animation => { + add_event_listener(any_el.clone(), ev::animationend, move |_| { + end_count.update_value(|v| { + *v += 1; + if *v >= prop_count { + remove(); + } + }); + }) + } + }; + end_handle.set_value(Some(handle)); }); let on_enter_fn = { @@ -130,9 +166,81 @@ where children(display.read_only()) } - -pub fn next_frame(cb: impl FnOnce() + 'static) { +fn next_frame(cb: impl FnOnce() + 'static) { request_animation_frame(move || { request_animation_frame(cb); }); -} \ No newline at end of file +} + +#[derive(PartialEq)] +enum AnimationTypes { + Transition, + Animation, +} + +#[derive(Default)] +struct CSSTransitionInfo { + types: Option, + prop_count: usize, + timeout: u64, +} + +fn get_transition_info(el: &web_sys::HtmlElement) -> Option { + 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::>() + }; + + 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 { + (Some(AnimationTypes::Transition), transition_durations.len()) + } else { + (Some(AnimationTypes::Animation), animation_durations.len()) + } + } else { + (None, 0) + }; + + Some(CSSTransitionInfo { + types, + prop_count, + timeout, + }) +} + +fn get_timeout(mut delays: Vec, durations: &Vec) -> 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::().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() +} From 166f6c481cb8a85a11cb39eb9f8c8f4d8a6e7467 Mon Sep 17 00:00:00 2001 From: luoxiao Date: Mon, 4 Mar 2024 15:55:27 +0800 Subject: [PATCH 3/4] fix: CSSTransition repeated processing --- thaw/src/components/css_transition/mod.rs | 74 +++++++++++++---------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/thaw/src/components/css_transition/mod.rs b/thaw/src/components/css_transition/mod.rs index ca9a9ef4..437021cd 100644 --- a/thaw/src/components/css_transition/mod.rs +++ b/thaw/src/components/css_transition/mod.rs @@ -27,7 +27,8 @@ where let el = any_el.deref().clone(); let class_list = el.class_list(); let end_handle = StoredValue::new(None::); - let end_count = StoredValue::new(0usize); + let end_count = StoredValue::new(None::); + let finish = StoredValue::new(None::>); let on_end = Callback::new(move |remove: Callback<()>| { let Some(CSSTransitionInfo { @@ -39,56 +40,62 @@ where remove.call(()); return; }; - let Some(types) = types else { - remove.call(()); - return; - }; - let remove = move || { + 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(); }); }); - }; - - end_count.set_value(0); + }))); set_timeout( move || { - if end_count.with_value(|v| v < &prop_count) { - remove(); - } + 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 |_| { - end_count.update_value(|v| { - *v += 1; - if *v >= prop_count { - remove(); - } - }); - }) + add_event_listener(any_el.clone(), ev::transitionend, move |_| event_listener()) } AnimationTypes::Animation => { - add_event_listener(any_el.clone(), ev::animationend, move |_| { - end_count.update_value(|v| { - *v += 1; - if *v >= prop_count { - remove(); - } - }); - }) + add_event_listener(any_el.clone(), ev::animationend, move |_| event_listener()) } }; end_handle.set_value(Some(handle)); }); + let on_finish = move || { + finish.update_value(|v| { + v.take().map(|f| f.call(())); + }); + }; + let on_enter_fn = { let class_list = class_list.clone(); Callback::new(move |name: String| { @@ -154,8 +161,10 @@ where 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); } @@ -178,9 +187,8 @@ enum AnimationTypes { Animation, } -#[derive(Default)] struct CSSTransitionInfo { - types: Option, + types: AnimationTypes, prop_count: usize, timeout: u64, } @@ -207,12 +215,12 @@ fn get_transition_info(el: &web_sys::HtmlElement) -> Option { let timeout = u64::max(transition_timeout, animation_timeout); let (types, prop_count) = if timeout > 0 { if transition_timeout > animation_timeout { - (Some(AnimationTypes::Transition), transition_durations.len()) + (AnimationTypes::Transition, transition_durations.len()) } else { - (Some(AnimationTypes::Animation), animation_durations.len()) + (AnimationTypes::Animation, animation_durations.len()) } } else { - (None, 0) + return None; }; Some(CSSTransitionInfo { From 7a3de1d76e0734f0da93e45ce658edf38e112d4f Mon Sep 17 00:00:00 2001 From: luoxiao Date: Mon, 4 Mar 2024 22:04:52 +0800 Subject: [PATCH 4/4] fix: Modal css transition --- thaw/src/modal/mod.rs | 32 +++++++++++++++++++++----------- thaw/src/modal/modal.css | 12 ++++++------ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/thaw/src/modal/mod.rs b/thaw/src/modal/mod.rs index 67c30306..837e38b0 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 > - -
-
} diff --git a/thaw/src/modal/modal.css b/thaw/src/modal/modal.css index 6029ec32..97efb784 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); }