mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-08 19:03:09 -05:00
feat: binder component add scroll listener
This commit is contained in:
parent
c54482d020
commit
6ca2b1de3c
6 changed files with 267 additions and 103 deletions
|
@ -1,103 +0,0 @@
|
||||||
use crate::teleport::Teleport;
|
|
||||||
use leptos::{
|
|
||||||
html::{AnyElement, ToHtmlElement},
|
|
||||||
*,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[slot]
|
|
||||||
pub struct Follower {
|
|
||||||
show: MaybeSignal<bool>,
|
|
||||||
children: Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Binder(
|
|
||||||
target: NodeRef<AnyElement>,
|
|
||||||
follower: Follower,
|
|
||||||
children: Children,
|
|
||||||
) -> impl IntoView {
|
|
||||||
let scrollable_element_vec = store_value::<Vec<HtmlElement<AnyElement>>>(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()}
|
|
||||||
<Teleport>
|
|
||||||
<div class="thaw-binder-follower-container">
|
|
||||||
<div class="thaw-binder-follower-content">
|
|
||||||
{(follower.children)()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_scroll_parent(element: Option<HtmlElement<AnyElement>>) -> Option<HtmlElement<AnyElement>> {
|
|
||||||
let Some(element) = element else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
fn get_parent_element(element: HtmlElement<AnyElement>) -> Option<HtmlElement<AnyElement>> {
|
|
||||||
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<AnyElement>,
|
|
||||||
) -> 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))
|
|
||||||
}
|
|
13
src/components/binder/binder.css
Normal file
13
src/components/binder/binder.css
Normal file
|
@ -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;
|
||||||
|
}
|
200
src/components/binder/mod.rs
Normal file
200
src/components/binder/mod.rs
Normal file
|
@ -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<bool>,
|
||||||
|
children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Binder<El: ElementDescriptor + Clone + 'static>(
|
||||||
|
#[prop(into)] target_ref: NodeRef<El>,
|
||||||
|
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::<Callback<()>>);
|
||||||
|
let scrollable_element_and_handle_vec =
|
||||||
|
store_value::<Vec<(HtmlElement<AnyElement>, Option<EventListenerHandle>)>>(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()}
|
||||||
|
<FollowerContainer show=follower_show target_ref add_scroll_listener remove_scroll_listener>
|
||||||
|
{follower_children()}
|
||||||
|
</FollowerContainer>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
|
||||||
|
show: MaybeSignal<bool>,
|
||||||
|
target_ref: NodeRef<El>,
|
||||||
|
#[prop(into)] add_scroll_listener: Callback<Callback<()>>,
|
||||||
|
#[prop(into)] remove_scroll_listener: Callback<()>,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let content_ref = create_node_ref::<html::Div>();
|
||||||
|
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! {
|
||||||
|
<Teleport>
|
||||||
|
<div class="thaw-binder-follower-container" style=move || {
|
||||||
|
if is_show.get() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"display: none;"
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<div class="thaw-binder-follower-content" ref=content_ref>
|
||||||
|
{children()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_scroll_parent(element: Option<HtmlElement<AnyElement>>) -> Option<HtmlElement<AnyElement>> {
|
||||||
|
let Some(element) = element else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_parent_element(element: HtmlElement<AnyElement>) -> Option<HtmlElement<AnyElement>> {
|
||||||
|
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<AnyElement>,
|
||||||
|
) -> 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))
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ mod binder;
|
||||||
mod if_comp;
|
mod if_comp;
|
||||||
mod option_comp;
|
mod option_comp;
|
||||||
|
|
||||||
|
pub use binder::*;
|
||||||
pub use if_comp::*;
|
pub use if_comp::*;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
pub use option_comp::*;
|
pub use option_comp::*;
|
||||||
|
|
51
src/utils/event_listener.rs
Normal file
51
src/utils/event_listener.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use leptos::*;
|
||||||
|
use wasm_bindgen::{prelude::Closure, JsCast};
|
||||||
|
|
||||||
|
pub fn add_event_listener<E: ev::EventDescriptor + 'static>(
|
||||||
|
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::<E::EventType>())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EventListenerHandle(Box<dyn FnOnce()>);
|
||||||
|
|
||||||
|
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<dyn FnMut(web_sys::Event)>,
|
||||||
|
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)
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
// mod callback;
|
// mod callback;
|
||||||
mod component_ref;
|
mod component_ref;
|
||||||
|
mod event_listener;
|
||||||
pub mod mount_style;
|
pub mod mount_style;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
mod stored_maybe_signal;
|
mod stored_maybe_signal;
|
||||||
|
|
||||||
// pub use callback::AsyncCallback;
|
// pub use callback::AsyncCallback;
|
||||||
pub use component_ref::ComponentRef;
|
pub use component_ref::ComponentRef;
|
||||||
|
pub use event_listener::*;
|
||||||
pub use stored_maybe_signal::*;
|
pub use stored_maybe_signal::*;
|
||||||
|
|
Loading…
Add table
Reference in a new issue