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); }