feat: binder component add resize listener

This commit is contained in:
luoxiao 2023-11-11 00:38:31 +08:00
parent 6ca2b1de3c
commit 7b44974533
4 changed files with 128 additions and 121 deletions

View file

@ -1,6 +1,5 @@
.thaw-auto-complete__menu { .thaw-auto-complete__menu {
position: relative; width: 100%;
display: inline-block;
max-height: 200px; max-height: 200px;
padding: 5px; padding: 5px;
background-color: var(--thaw-background-color); background-color: var(--thaw-background-color);
@ -9,7 +8,6 @@
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
overflow: auto; overflow: auto;
z-index: 2000;
} }
.thaw-auto-complete__menu-item { .thaw-auto-complete__menu-item {
padding: 6px 5px; padding: 6px 5px;

View file

@ -1,6 +1,11 @@
mod theme; mod theme;
use crate::{mount_style, teleport::Teleport, use_theme, utils::StoredMaybeSignal, Input, Theme}; use crate::{
components::{Binder, Follower},
mount_style, use_theme,
utils::StoredMaybeSignal,
Input, Theme,
};
use leptos::*; use leptos::*;
pub use theme::AutoCompleteTheme; pub use theme::AutoCompleteTheme;
@ -37,93 +42,66 @@ pub fn AutoComplete(
let is_show_menu = create_rw_signal(false); let is_show_menu = create_rw_signal(false);
let auto_complete_ref = create_node_ref::<html::Div>(); let auto_complete_ref = create_node_ref::<html::Div>();
let auto_complete_menu_ref = create_node_ref::<html::Div>();
let options = StoredMaybeSignal::from(options); let options = StoredMaybeSignal::from(options);
let show_menu = move || {
is_show_menu.set(true);
let rect = auto_complete_ref
.get_untracked()
.unwrap()
.get_bounding_client_rect();
let auto_complete_menu = auto_complete_menu_ref.get_untracked().unwrap();
_ = auto_complete_menu
.style("width", format!("{}px", rect.width()))
.style(
"transform",
format!(
"translateX({}px) translateY({}px)",
rect.x(),
rect.y() + rect.height()
),
);
};
let allow_value = move |_| { let allow_value = move |_| {
if !is_show_menu.get_untracked() { if !is_show_menu.get_untracked() {
show_menu(); is_show_menu.set(true);
} }
true true
}; };
view! { view! {
<div class="thaw-auto-complete" ref=auto_complete_ref> <Binder target_ref=auto_complete_ref>
<Input <div class="thaw-auto-complete" ref=auto_complete_ref>
value <Input
placeholder value
on_focus=move |_| show_menu() placeholder
on_blur=move |_| is_show_menu.set(false) on_focus=move |_| is_show_menu.set(true)
allow_value on_blur=move |_| is_show_menu.set(false)
/> allow_value
</div> />
<Teleport>
<div
class="thaw-auto-complete__menu"
style=move || {
if is_show_menu.get() {
menu_css_vars.get()
} else {
"display: none;".to_string()
}
}
ref=auto_complete_menu_ref
>
{move || {
options
.get()
.into_iter()
.map(|v| {
let AutoCompleteOption { value: option_value, label } = v;
let on_click = move |_| {
if clear_after_select.get_untracked() {
value.set(String::new());
} else {
value.set(option_value.clone());
}
if let Some(on_select) = on_select {
on_select.call(option_value.clone());
}
is_show_menu.set(false);
};
let on_mousedown = move |ev: ev::MouseEvent| {
ev.prevent_default();
};
view! {
<div
class="thaw-auto-complete__menu-item"
on:click=on_click
on:mousedown=on_mousedown
>
{label}
</div>
}
})
.collect_view()
}}
</div> </div>
</Teleport> <Follower slot show=is_show_menu>
<div
class="thaw-auto-complete__menu"
style=move || menu_css_vars.get()
>
{move || {
options
.get()
.into_iter()
.map(|v| {
let AutoCompleteOption { value: option_value, label } = v;
let on_click = move |_| {
if clear_after_select.get_untracked() {
value.set(String::new());
} else {
value.set(option_value.clone());
}
if let Some(on_select) = on_select {
on_select.call(option_value.clone());
}
is_show_menu.set(false);
};
let on_mousedown = move |ev: ev::MouseEvent| {
ev.prevent_default();
};
view! {
<div
class="thaw-auto-complete__menu-item"
on:click=on_click
on:mousedown=on_mousedown
>
{label}
</div>
}
})
.collect_view()
}}
</div>
</Follower>
</Binder>
} }
} }

View file

@ -5,6 +5,7 @@ use crate::{
}; };
use leptos::{ use leptos::{
html::{AnyElement, ElementDescriptor, ToHtmlElement}, html::{AnyElement, ElementDescriptor, ToHtmlElement},
leptos_dom::helpers::WindowListenerHandle,
*, *,
}; };
@ -26,32 +27,37 @@ pub fn Binder<El: ElementDescriptor + Clone + 'static>(
show: follower_show, show: follower_show,
children: follower_children, children: follower_children,
} = follower; } = follower;
let follower_scroll_listener = store_value(None::<Callback<()>>);
let scrollable_element_and_handle_vec = let scroll_listener = store_value(None::<Callback<()>>);
store_value::<Vec<(HtmlElement<AnyElement>, Option<EventListenerHandle>)>>(vec![]); let scrollable_element_handle_vec = store_value::<Vec<EventListenerHandle>>(vec![]);
let resize_handle = store_value(None::<WindowListenerHandle>);
let ensure_scroll_listener = move || { let ensure_scroll_listener = move || {
let mut cursor = target_ref.get_untracked().map(|target| target.into_any()); let mut cursor = target_ref.get_untracked().map(|target| target.into_any());
let mut scrollable_element_vec = vec![];
loop { loop {
cursor = get_scroll_parent(cursor); cursor = get_scroll_parent(cursor);
if let Some(cursor) = cursor.take() { if let Some(cursor) = cursor.take() {
scrollable_element_and_handle_vec.update_value(|vec| vec.push((cursor, None))); scrollable_element_vec.push(cursor);
} else { } else {
break; break;
} }
} }
scrollable_element_and_handle_vec.update_value(|vec| { let handle_vec = scrollable_element_vec
vec.iter_mut().for_each(|ele| { .into_iter()
ele.1 = Some(add_event_listener(&ele.0, ev::scroll, move |_| { .map(|ele| {
if let Some(follower_scroll_listener) = follower_scroll_listener.get_value() { add_event_listener(ele, ev::scroll, move |_| {
follower_scroll_listener.call(()); if let Some(scroll_listener) = scroll_listener.get_value() {
scroll_listener.call(());
} }
})); })
}) })
}); .collect();
scrollable_element_handle_vec.set_value(handle_vec);
}; };
let add_scroll_listener = move |listener: Callback<()>| { let add_scroll_listener = move |listener: Callback<()>| {
follower_scroll_listener.update_value(|scroll_listener| { scroll_listener.update_value(|scroll_listener| {
if scroll_listener.is_none() { if scroll_listener.is_none() {
ensure_scroll_listener(); ensure_scroll_listener();
} }
@ -60,22 +66,47 @@ pub fn Binder<El: ElementDescriptor + Clone + 'static>(
}; };
let remove_scroll_listener = move |_| { let remove_scroll_listener = move |_| {
scrollable_element_and_handle_vec.update_value(|vec| { scrollable_element_handle_vec.update_value(|vec| {
let vec: Vec<_> = vec.drain(..).collect(); vec.drain(..).into_iter().for_each(|handle| handle.remove());
for item in vec { });
if let Some(handle) = item.1 { scroll_listener.set_value(None);
handle.remove(); };
}
let add_resize_listener = move |listener: Callback<()>| {
resize_handle.update_value(move |resize_handle| {
if let Some(handle) = resize_handle.take() {
handle.remove();
}
let handle = window_event_listener(ev::resize, move |_| {
listener.call(());
});
*resize_handle = Some(handle);
});
};
let remove_resize_listener = move |_| {
resize_handle.update_value(move |handle| {
if let Some(handle) = handle.take() {
handle.remove();
} }
}); });
}; };
on_cleanup(move || { on_cleanup(move || {
remove_scroll_listener(()); remove_scroll_listener(());
remove_resize_listener(());
}); });
view! { view! {
{children()} {children()}
<FollowerContainer show=follower_show target_ref add_scroll_listener remove_scroll_listener> <FollowerContainer
show=follower_show
target_ref
add_scroll_listener
remove_scroll_listener
add_resize_listener
remove_resize_listener
>
{follower_children()} {follower_children()}
</FollowerContainer> </FollowerContainer>
} }
@ -87,27 +118,26 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
target_ref: NodeRef<El>, target_ref: NodeRef<El>,
#[prop(into)] add_scroll_listener: Callback<Callback<()>>, #[prop(into)] add_scroll_listener: Callback<Callback<()>>,
#[prop(into)] remove_scroll_listener: Callback<()>, #[prop(into)] remove_scroll_listener: Callback<()>,
#[prop(into)] add_resize_listener: Callback<Callback<()>>,
#[prop(into)] remove_resize_listener: Callback<()>,
children: Children, children: Children,
) -> impl IntoView { ) -> impl IntoView {
let content_ref = create_node_ref::<html::Div>(); let content_ref = create_node_ref::<html::Div>();
let content_style = create_rw_signal(String::new());
let sync_position: Callback<()> = Callback::new(move |_| { 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 { let Some(target_ref) = target_ref.get().map(|target| target.into_any()) else {
return; return;
}; };
let target_rect = target_ref.get_bounding_client_rect(); let target_rect = target_ref.get_bounding_client_rect();
_ = content_ref
.style("width", format!("{}px", target_rect.width())) let mut style = String::new();
.style( style.push_str(&format!("width: {}px;", target_rect.width()));
"transform", style.push_str(&format!(
format!( "transform: translateX({}px) translateY({}px);",
"translateX({}px) translateY({}px)", target_rect.x(),
target_rect.x(), target_rect.y() + target_rect.height()
target_rect.y() + target_rect.height() ));
), content_style.set(style);
);
}); });
let is_show = create_memo(move |_| { let is_show = create_memo(move |_| {
@ -121,8 +151,10 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
if is_show { if is_show {
sync_position.call(()); sync_position.call(());
add_scroll_listener.call(sync_position); add_scroll_listener.call(sync_position);
add_resize_listener.call(sync_position);
} else { } else {
remove_scroll_listener.call(()); remove_scroll_listener.call(());
remove_resize_listener.call(());
} }
is_show is_show
}); });
@ -135,7 +167,7 @@ fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
"display: none;" "display: none;"
} }
}> }>
<div class="thaw-binder-follower-content" ref=content_ref> <div class="thaw-binder-follower-content" ref=content_ref style=move || content_style.get()>
{children()} {children()}
</div> </div>
</div> </div>

View file

@ -1,8 +1,8 @@
use leptos::*; use leptos::{html::AnyElement, *};
use wasm_bindgen::{prelude::Closure, JsCast}; use wasm_bindgen::{prelude::Closure, JsCast};
pub fn add_event_listener<E: ev::EventDescriptor + 'static>( pub fn add_event_listener<E: ev::EventDescriptor + 'static>(
target: &web_sys::Element, target: HtmlElement<AnyElement>,
event: E, event: E,
cb: impl Fn(E::EventType) + 'static, cb: impl Fn(E::EventType) + 'static,
) -> EventListenerHandle ) -> EventListenerHandle
@ -29,19 +29,18 @@ impl EventListenerHandle {
} }
fn add_event_listener_untyped( fn add_event_listener_untyped(
target: &web_sys::Element, target: HtmlElement<AnyElement>,
event_name: &str, event_name: &str,
cb: impl Fn(web_sys::Event) + 'static, cb: impl Fn(web_sys::Event) + 'static,
) -> EventListenerHandle { ) -> EventListenerHandle {
fn wel( fn wel(
target: &web_sys::Element, target: HtmlElement<AnyElement>,
cb: Box<dyn FnMut(web_sys::Event)>, cb: Box<dyn FnMut(web_sys::Event)>,
event_name: &str, event_name: &str,
) -> EventListenerHandle { ) -> EventListenerHandle {
let cb = Closure::wrap(cb).into_js_value(); let cb = Closure::wrap(cb).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref()); _ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
let event_name = event_name.to_string(); let event_name = event_name.to_string();
let target = target.clone();
EventListenerHandle(Box::new(move || { EventListenerHandle(Box::new(move || {
_ = target.remove_event_listener_with_callback(&event_name, cb.unchecked_ref()); _ = target.remove_event_listener_with_callback(&event_name, cb.unchecked_ref());
})) }))