From 6ca2b1de3ca74921a4475e1fa62e4d509bfcdec2 Mon Sep 17 00:00:00 2001 From: luoxiao Date: Fri, 10 Nov 2023 17:30:03 +0800 Subject: [PATCH] feat: binder component add scroll listener --- src/components/binder.rs | 103 ---------------- src/components/binder/binder.css | 13 ++ src/components/binder/mod.rs | 200 +++++++++++++++++++++++++++++++ src/components/mod.rs | 1 + src/utils/event_listener.rs | 51 ++++++++ src/utils/mod.rs | 2 + 6 files changed, 267 insertions(+), 103 deletions(-) delete mode 100644 src/components/binder.rs create mode 100644 src/components/binder/binder.css create mode 100644 src/components/binder/mod.rs create mode 100644 src/utils/event_listener.rs diff --git a/src/components/binder.rs b/src/components/binder.rs deleted file mode 100644 index 456ac06..0000000 --- a/src/components/binder.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::teleport::Teleport; -use leptos::{ - html::{AnyElement, ToHtmlElement}, - *, -}; - -#[slot] -pub struct Follower { - show: MaybeSignal, - children: Children, -} - -#[component] -pub fn Binder( - target: NodeRef, - follower: Follower, - children: Children, -) -> impl IntoView { - let scrollable_element_vec = store_value::>>(vec![]); - let ensure_scroll_listener = move || { - let mut cursor = target.get_untracked(); - loop { - cursor = get_scroll_parent(cursor); - if let Some(cursor) = cursor.take() { - scrollable_element_vec.update_value(|vec| vec.push(cursor)); - } else { - break; - } - } - scrollable_element_vec.with_value(|vec| { - vec.iter().for_each(|ele| { - _ = ele.clone().on(ev::scroll, move |_| {}); - }) - }); - }; - - view! { - {children()} - -
-
- {(follower.children)()} -
-
-
- } -} - -fn get_scroll_parent(element: Option>) -> Option> { - let Some(element) = element else { - return None; - }; - - fn get_parent_element(element: HtmlElement) -> Option> { - if element.node_type() == 9 { - None - } else { - element.parent_element().map(|ele| ele.to_leptos_element()) - } - } - let Some(parent_element) = get_parent_element(element) else { - return None; - }; - - if parent_element.node_type() == 9 { - return Some(parent_element); - } - - if parent_element.node_type() == 1 { - fn get_overflow( - parent_element: &HtmlElement, - ) -> Option<(String, String, String)> { - let Ok(Some(css_style_declaration)) = window().get_computed_style(parent_element) - else { - return None; - }; - let Ok(overflow) = css_style_declaration.get_property_value("overflow") else { - return None; - }; - let Ok(overflow_x) = css_style_declaration.get_property_value("overflowX") else { - return None; - }; - let Ok(overflow_y) = css_style_declaration.get_property_value("overflowY") else { - return None; - }; - Some((overflow, overflow_x, overflow_y)) - } - if let Some((overflow, overflow_x, overflow_y)) = get_overflow(&parent_element) { - let overflow = format!("{overflow}{overflow_x}{overflow_y}"); - if overflow.contains("auto") { - return Some(parent_element); - } - if overflow.contains("scroll") { - return Some(parent_element); - } - if overflow.contains("overlay") { - return Some(parent_element); - } - } - } - - get_scroll_parent(Some(parent_element)) -} diff --git a/src/components/binder/binder.css b/src/components/binder/binder.css new file mode 100644 index 0000000..0be166c --- /dev/null +++ b/src/components/binder/binder.css @@ -0,0 +1,13 @@ +.thaw-binder-follower-container { + position: absolute; + left: 0; + right: 0; + top: 0; + height: 0; + z-index: auto; +} + +.thaw-binder-follower-content { + position: absolute; + z-index: 2000; +} diff --git a/src/components/binder/mod.rs b/src/components/binder/mod.rs new file mode 100644 index 0000000..13bb582 --- /dev/null +++ b/src/components/binder/mod.rs @@ -0,0 +1,200 @@ +use crate::{ + mount_style, + teleport::Teleport, + utils::{add_event_listener, EventListenerHandle}, +}; +use leptos::{ + html::{AnyElement, ElementDescriptor, ToHtmlElement}, + *, +}; + +#[slot] +pub struct Follower { + #[prop(into)] + show: MaybeSignal, + children: Children, +} + +#[component] +pub fn Binder( + #[prop(into)] target_ref: NodeRef, + follower: Follower, + children: Children, +) -> impl IntoView { + mount_style("binder", include_str!("./binder.css")); + let Follower { + show: follower_show, + children: follower_children, + } = follower; + let follower_scroll_listener = store_value(None::>); + let scrollable_element_and_handle_vec = + store_value::, Option)>>(vec![]); + let ensure_scroll_listener = move || { + let mut cursor = target_ref.get_untracked().map(|target| target.into_any()); + loop { + cursor = get_scroll_parent(cursor); + if let Some(cursor) = cursor.take() { + scrollable_element_and_handle_vec.update_value(|vec| vec.push((cursor, None))); + } else { + break; + } + } + scrollable_element_and_handle_vec.update_value(|vec| { + vec.iter_mut().for_each(|ele| { + ele.1 = Some(add_event_listener(&ele.0, ev::scroll, move |_| { + if let Some(follower_scroll_listener) = follower_scroll_listener.get_value() { + follower_scroll_listener.call(()); + } + })); + }) + }); + }; + + let add_scroll_listener = move |listener: Callback<()>| { + follower_scroll_listener.update_value(|scroll_listener| { + if scroll_listener.is_none() { + ensure_scroll_listener(); + } + *scroll_listener = Some(listener); + }) + }; + + let remove_scroll_listener = move |_| { + scrollable_element_and_handle_vec.update_value(|vec| { + let vec: Vec<_> = vec.drain(..).collect(); + for item in vec { + if let Some(handle) = item.1 { + handle.remove(); + } + } + }); + }; + + on_cleanup(move || { + remove_scroll_listener(()); + }); + view! { + {children()} + + {follower_children()} + + } +} + +#[component] +fn FollowerContainer( + show: MaybeSignal, + target_ref: NodeRef, + #[prop(into)] add_scroll_listener: Callback>, + #[prop(into)] remove_scroll_listener: Callback<()>, + children: Children, +) -> impl IntoView { + let content_ref = create_node_ref::(); + let sync_position: Callback<()> = Callback::new(move |_| { + let Some(content_ref) = content_ref.get() else { + return; + }; + let Some(target_ref) = target_ref.get().map(|target| target.into_any()) else { + return; + }; + let target_rect = target_ref.get_bounding_client_rect(); + _ = content_ref + .style("width", format!("{}px", target_rect.width())) + .style( + "transform", + format!( + "translateX({}px) translateY({}px)", + target_rect.x(), + target_rect.y() + target_rect.height() + ), + ); + }); + + let is_show = create_memo(move |_| { + if target_ref.get().is_none() { + return false; + } + if content_ref.get().is_none() { + return false; + } + let is_show = show.get(); + if is_show { + sync_position.call(()); + add_scroll_listener.call(sync_position); + } else { + remove_scroll_listener.call(()); + } + is_show + }); + view! { + +
+
+ {children()} +
+
+
+ } +} + +fn get_scroll_parent(element: Option>) -> Option> { + let Some(element) = element else { + return None; + }; + + fn get_parent_element(element: HtmlElement) -> Option> { + if element.node_type() == 9 { + None + } else { + element.parent_element().map(|ele| ele.to_leptos_element()) + } + } + let Some(parent_element) = get_parent_element(element) else { + return None; + }; + + if parent_element.node_type() == 9 { + return Some(parent_element); + } + + if parent_element.node_type() == 1 { + fn get_overflow( + parent_element: &HtmlElement, + ) -> Option<(String, String, String)> { + let Ok(Some(css_style_declaration)) = window().get_computed_style(parent_element) + else { + return None; + }; + let Ok(overflow) = css_style_declaration.get_property_value("overflow") else { + return None; + }; + let Ok(overflow_x) = css_style_declaration.get_property_value("overflowX") else { + return None; + }; + let Ok(overflow_y) = css_style_declaration.get_property_value("overflowY") else { + return None; + }; + Some((overflow, overflow_x, overflow_y)) + } + if let Some((overflow, overflow_x, overflow_y)) = get_overflow(&parent_element) { + let overflow = format!("{overflow}{overflow_x}{overflow_y}"); + if overflow.contains("auto") { + return Some(parent_element); + } + if overflow.contains("scroll") { + return Some(parent_element); + } + if overflow.contains("overlay") { + return Some(parent_element); + } + } + } + + get_scroll_parent(Some(parent_element)) +} diff --git a/src/components/mod.rs b/src/components/mod.rs index e0d6b10..db9eeca 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -2,6 +2,7 @@ mod binder; mod if_comp; mod option_comp; +pub use binder::*; pub use if_comp::*; use leptos::*; pub use option_comp::*; diff --git a/src/utils/event_listener.rs b/src/utils/event_listener.rs new file mode 100644 index 0000000..6b5b8e7 --- /dev/null +++ b/src/utils/event_listener.rs @@ -0,0 +1,51 @@ +use leptos::*; +use wasm_bindgen::{prelude::Closure, JsCast}; + +pub fn add_event_listener( + target: &web_sys::Element, + event: E, + cb: impl Fn(E::EventType) + 'static, +) -> EventListenerHandle +where + E::EventType: JsCast, +{ + add_event_listener_untyped(target, &event.name(), move |e| { + cb(e.unchecked_into::()) + }) +} + +pub struct EventListenerHandle(Box); + +impl std::fmt::Debug for EventListenerHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("EventListenerHandle").finish() + } +} + +impl EventListenerHandle { + pub fn remove(self) { + (self.0)() + } +} + +fn add_event_listener_untyped( + target: &web_sys::Element, + event_name: &str, + cb: impl Fn(web_sys::Event) + 'static, +) -> EventListenerHandle { + fn wel( + target: &web_sys::Element, + cb: Box, + event_name: &str, + ) -> EventListenerHandle { + let cb = Closure::wrap(cb).into_js_value(); + _ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref()); + let event_name = event_name.to_string(); + let target = target.clone(); + EventListenerHandle(Box::new(move || { + _ = target.remove_event_listener_with_callback(&event_name, cb.unchecked_ref()); + })) + } + + wel(target, Box::new(cb), event_name) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 5c7526e..f2237aa 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,9 +1,11 @@ // mod callback; mod component_ref; +mod event_listener; pub mod mount_style; pub mod signal; mod stored_maybe_signal; // pub use callback::AsyncCallback; pub use component_ref::ComponentRef; +pub use event_listener::*; pub use stored_maybe_signal::*;