mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-23 06:19:22 -05:00
feat: binder component add resize listener
This commit is contained in:
parent
6ca2b1de3c
commit
7b44974533
4 changed files with 128 additions and 121 deletions
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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());
|
||||||
}))
|
}))
|
||||||
|
|
Loading…
Add table
Reference in a new issue