mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-23 14:29:22 -05:00
commit
34dfe2e553
13 changed files with 534 additions and 188 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, FollowerPlacement, FollowerWidth},
|
||||||
|
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 placement=FollowerPlacement::BottomStart width=FollowerWidth::Target>
|
||||||
|
<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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-color-picker-popover {
|
.thaw-color-picker-popover {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 240px;
|
width: 240px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background-color: var(--thaw-background-color);
|
background-color: var(--thaw-background-color);
|
||||||
|
@ -29,7 +25,6 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
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);
|
||||||
z-index: 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-color-picker-popover__panel {
|
.thaw-color-picker-popover__panel {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
mod color;
|
mod color;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
use crate::{mount_style, teleport::Teleport, use_theme, Theme};
|
use crate::components::{Binder, Follower, FollowerPlacement};
|
||||||
|
use crate::{mount_style, use_theme, Theme};
|
||||||
pub use color::*;
|
pub use color::*;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos::{leptos_dom::helpers::WindowListenerHandle, wasm_bindgen::__rt::IntoJsResult};
|
use leptos::{leptos_dom::helpers::WindowListenerHandle, wasm_bindgen::__rt::IntoJsResult};
|
||||||
|
@ -62,18 +63,7 @@ pub fn ColorPicker(#[prop(optional, into)] value: RwSignal<RGBA>) -> impl IntoVi
|
||||||
let trigger_ref = create_node_ref::<html::Div>();
|
let trigger_ref = create_node_ref::<html::Div>();
|
||||||
let popover_ref = create_node_ref::<html::Div>();
|
let popover_ref = create_node_ref::<html::Div>();
|
||||||
let show_popover = move |_| {
|
let show_popover = move |_| {
|
||||||
let rect = trigger_ref.get().unwrap().get_bounding_client_rect();
|
|
||||||
is_show_popover.set(true);
|
is_show_popover.set(true);
|
||||||
if let Some(popover_ref) = popover_ref.get() {
|
|
||||||
_ = popover_ref.style(
|
|
||||||
"transform",
|
|
||||||
format!(
|
|
||||||
"translateX({}px) translateY({}px)",
|
|
||||||
rect.x(),
|
|
||||||
rect.y() + rect.height()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let timer = window_event_listener(ev::click, move |ev| {
|
let timer = window_event_listener(ev::click, move |ev| {
|
||||||
let el = ev.target();
|
let el = ev.target();
|
||||||
|
@ -96,28 +86,24 @@ pub fn ColorPicker(#[prop(optional, into)] value: RwSignal<RGBA>) -> impl IntoVi
|
||||||
on_cleanup(move || timer.remove());
|
on_cleanup(move || timer.remove());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="thaw-color-picker-trigger" on:click=show_popover ref=trigger_ref>
|
<Binder target_ref=trigger_ref>
|
||||||
<div class="thaw-color-picker-trigger__content" style=move || style.get()>
|
<div class="thaw-color-picker-trigger" on:click=show_popover ref=trigger_ref>
|
||||||
{move || label.get()}
|
<div class="thaw-color-picker-trigger__content" style=move || style.get()>
|
||||||
|
{move || label.get()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Follower slot show=is_show_popover placement=FollowerPlacement::BottomStart>
|
||||||
<Teleport>
|
<div
|
||||||
<div
|
class="thaw-color-picker-popover"
|
||||||
class="thaw-color-picker-popover"
|
ref=popover_ref
|
||||||
ref=popover_ref
|
style=move || popover_css_vars.get()
|
||||||
style=move || {
|
>
|
||||||
if is_show_popover.get() {
|
|
||||||
popover_css_vars.get()
|
|
||||||
} else {
|
|
||||||
"display: none".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
<ColorPanel hue=hue.read_only() sv/>
|
<ColorPanel hue=hue.read_only() sv/>
|
||||||
<HueSlider hue/>
|
<HueSlider hue/>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Follower>
|
||||||
|
</Binder>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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;
|
||||||
|
}
|
63
src/components/binder/get_placement_style.rs
Normal file
63
src/components/binder/get_placement_style.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use leptos::window;
|
||||||
|
use web_sys::DomRect;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum FollowerPlacement {
|
||||||
|
// Top,
|
||||||
|
// Bottom,
|
||||||
|
// Left,
|
||||||
|
// Right,
|
||||||
|
// TopStart,
|
||||||
|
// TopEnd,
|
||||||
|
// LeftStart,
|
||||||
|
// LeftEnd,
|
||||||
|
// RightStart,
|
||||||
|
// RightEnd,
|
||||||
|
BottomStart,
|
||||||
|
// BottomEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Copy for FollowerPlacement {}
|
||||||
|
|
||||||
|
pub fn get_follower_placement_style(
|
||||||
|
placement: FollowerPlacement,
|
||||||
|
target_rect: DomRect,
|
||||||
|
follower_rect: DomRect,
|
||||||
|
) -> Option<String> {
|
||||||
|
// TODO: Implements FollowerPlacement more properties
|
||||||
|
_ = placement;
|
||||||
|
let mut style = String::new();
|
||||||
|
let left = target_rect.x();
|
||||||
|
let top = {
|
||||||
|
let follower_height = follower_rect.height();
|
||||||
|
let target_y = target_rect.y();
|
||||||
|
let target_height = target_rect.height();
|
||||||
|
let top = target_y + target_height;
|
||||||
|
|
||||||
|
let Some(inner_height) = window_inner_height() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if top + follower_height > inner_height && target_y - follower_height >= 0.0 {
|
||||||
|
target_y - follower_height
|
||||||
|
} else {
|
||||||
|
top
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
style.push_str(&format!(
|
||||||
|
"transform: translateX({left}px) translateY({top}px);"
|
||||||
|
));
|
||||||
|
|
||||||
|
Some(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_inner_height() -> Option<f64> {
|
||||||
|
let Ok(inner_height) = window().inner_height() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Some(inner_height) = inner_height.as_f64() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(inner_height)
|
||||||
|
}
|
266
src/components/binder/mod.rs
Normal file
266
src/components/binder/mod.rs
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
mod get_placement_style;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
mount_style,
|
||||||
|
teleport::Teleport,
|
||||||
|
utils::{add_event_listener, EventListenerHandle},
|
||||||
|
};
|
||||||
|
use get_placement_style::get_follower_placement_style;
|
||||||
|
pub use get_placement_style::FollowerPlacement;
|
||||||
|
use leptos::{
|
||||||
|
html::{AnyElement, ElementDescriptor, ToHtmlElement},
|
||||||
|
leptos_dom::helpers::WindowListenerHandle,
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[slot]
|
||||||
|
pub struct Follower {
|
||||||
|
#[prop(into)]
|
||||||
|
show: MaybeSignal<bool>,
|
||||||
|
#[prop(optional)]
|
||||||
|
width: Option<FollowerWidth>,
|
||||||
|
placement: FollowerPlacement,
|
||||||
|
children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum FollowerWidth {
|
||||||
|
Target,
|
||||||
|
Px(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Copy for FollowerWidth {}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
width: follower_width,
|
||||||
|
placement: follower_placement,
|
||||||
|
children: follower_children,
|
||||||
|
} = follower;
|
||||||
|
|
||||||
|
let scroll_listener = store_value(None::<Callback<()>>);
|
||||||
|
let scrollable_element_handle_vec = store_value::<Vec<EventListenerHandle>>(vec![]);
|
||||||
|
let resize_handle = store_value(None::<WindowListenerHandle>);
|
||||||
|
|
||||||
|
let ensure_scroll_listener = move || {
|
||||||
|
let mut cursor = target_ref.get_untracked().map(|target| target.into_any());
|
||||||
|
let mut scrollable_element_vec = vec![];
|
||||||
|
loop {
|
||||||
|
cursor = get_scroll_parent(cursor);
|
||||||
|
if let Some(cursor) = cursor.take() {
|
||||||
|
scrollable_element_vec.push(cursor);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let handle_vec = scrollable_element_vec
|
||||||
|
.into_iter()
|
||||||
|
.map(|ele| {
|
||||||
|
add_event_listener(ele, ev::scroll, move |_| {
|
||||||
|
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<()>| {
|
||||||
|
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_handle_vec.update_value(|vec| {
|
||||||
|
vec.drain(..).for_each(|handle| handle.remove());
|
||||||
|
});
|
||||||
|
scroll_listener.set_value(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 || {
|
||||||
|
remove_scroll_listener(());
|
||||||
|
remove_resize_listener(());
|
||||||
|
});
|
||||||
|
view! {
|
||||||
|
{children()}
|
||||||
|
<FollowerContainer
|
||||||
|
show=follower_show
|
||||||
|
target_ref
|
||||||
|
width=follower_width
|
||||||
|
placement=follower_placement
|
||||||
|
add_scroll_listener
|
||||||
|
remove_scroll_listener
|
||||||
|
add_resize_listener
|
||||||
|
remove_resize_listener
|
||||||
|
>
|
||||||
|
{follower_children()}
|
||||||
|
</FollowerContainer>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn FollowerContainer<El: ElementDescriptor + Clone + 'static>(
|
||||||
|
show: MaybeSignal<bool>,
|
||||||
|
target_ref: NodeRef<El>,
|
||||||
|
width: Option<FollowerWidth>,
|
||||||
|
placement: FollowerPlacement,
|
||||||
|
#[prop(into)] add_scroll_listener: Callback<Callback<()>>,
|
||||||
|
#[prop(into)] remove_scroll_listener: Callback<()>,
|
||||||
|
#[prop(into)] add_resize_listener: Callback<Callback<()>>,
|
||||||
|
#[prop(into)] remove_resize_listener: Callback<()>,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let content_ref = create_node_ref::<html::Div>();
|
||||||
|
let content_style = create_rw_signal(String::new());
|
||||||
|
let sync_position: Callback<()> = Callback::new(move |_| {
|
||||||
|
let Some(content_ref) = content_ref.get_untracked() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(target_ref) = target_ref.get_untracked().map(|target| target.into_any()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let target_rect = target_ref.get_bounding_client_rect();
|
||||||
|
let content_rect = content_ref.get_bounding_client_rect();
|
||||||
|
let mut style = String::new();
|
||||||
|
if let Some(width) = width {
|
||||||
|
let width = match width {
|
||||||
|
FollowerWidth::Target => format!("width: {}px;", target_rect.width()),
|
||||||
|
FollowerWidth::Px(width) => format!("width: {width}px;"),
|
||||||
|
};
|
||||||
|
style.push_str(&width);
|
||||||
|
}
|
||||||
|
if let Some(placement_style) =
|
||||||
|
get_follower_placement_style(placement, target_rect, content_rect)
|
||||||
|
{
|
||||||
|
style.push_str(&placement_style);
|
||||||
|
} else {
|
||||||
|
logging::error!("Thaw-Binder: get_follower_placement_style return None");
|
||||||
|
}
|
||||||
|
|
||||||
|
content_style.set(style);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
||||||
|
request_animation_frame(move || {
|
||||||
|
sync_position.call(());
|
||||||
|
});
|
||||||
|
add_scroll_listener.call(sync_position);
|
||||||
|
add_resize_listener.call(sync_position);
|
||||||
|
} else {
|
||||||
|
remove_scroll_listener.call(());
|
||||||
|
remove_resize_listener.call(());
|
||||||
|
}
|
||||||
|
is_show
|
||||||
|
});
|
||||||
|
|
||||||
|
let children = html::div()
|
||||||
|
.classes("thaw-binder-follower-container")
|
||||||
|
.style("display", move || (!is_show.get()).then_some("none"))
|
||||||
|
.child(
|
||||||
|
html::div()
|
||||||
|
.classes("thaw-binder-follower-content")
|
||||||
|
.node_ref(content_ref)
|
||||||
|
.attr("style", move || content_style.get())
|
||||||
|
.child(children()),
|
||||||
|
);
|
||||||
|
view! {
|
||||||
|
<Teleport element=children />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
|
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::*;
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
use crate::{teleport::Teleport, theme::use_theme, utils::mount_style::mount_style, Theme};
|
use crate::{
|
||||||
|
components::{Binder, Follower, FollowerPlacement, FollowerWidth},
|
||||||
|
theme::use_theme,
|
||||||
|
utils::mount_style::mount_style,
|
||||||
|
Theme,
|
||||||
|
};
|
||||||
use leptos::wasm_bindgen::__rt::IntoJsResult;
|
use leptos::wasm_bindgen::__rt::IntoJsResult;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
@ -66,20 +71,7 @@ where
|
||||||
let trigger_ref = create_node_ref::<html::Div>();
|
let trigger_ref = create_node_ref::<html::Div>();
|
||||||
let menu_ref = create_node_ref::<html::Div>();
|
let menu_ref = create_node_ref::<html::Div>();
|
||||||
let show_menu = move |_| {
|
let show_menu = move |_| {
|
||||||
let rect = trigger_ref.get().unwrap().get_bounding_client_rect();
|
|
||||||
is_show_menu.set(true);
|
is_show_menu.set(true);
|
||||||
if let Some(menu_ref) = menu_ref.get() {
|
|
||||||
_ = menu_ref
|
|
||||||
.style("width", format!("{}px", rect.width()))
|
|
||||||
.style(
|
|
||||||
"transform",
|
|
||||||
format!(
|
|
||||||
"translateX({}px) translateY({}px) translateX(-50%)",
|
|
||||||
rect.x() + rect.width() / 2.0,
|
|
||||||
rect.y() + rect.height()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let timer = window_event_listener(ev::click, move |ev| {
|
let timer = window_event_listener(ev::click, move |ev| {
|
||||||
let el = ev.target();
|
let el = ev.target();
|
||||||
|
@ -111,47 +103,46 @@ where
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
});
|
});
|
||||||
view! {
|
view! {
|
||||||
<div class="thaw-select" ref=trigger_ref on:click=show_menu style=move || css_vars.get()>
|
<Binder target_ref=trigger_ref>
|
||||||
|
<div class="thaw-select" ref=trigger_ref on:click=show_menu style=move || css_vars.get()>
|
||||||
|
|
||||||
{move || select_option_label.get()}
|
{move || select_option_label.get()}
|
||||||
|
|
||||||
</div>
|
|
||||||
<Teleport>
|
|
||||||
<div
|
|
||||||
class="thaw-select-menu"
|
|
||||||
style=move || {
|
|
||||||
if is_show_menu.get() { menu_css_vars.get() } else { "display: none;".into() }
|
|
||||||
}
|
|
||||||
|
|
||||||
ref=menu_ref
|
|
||||||
>
|
|
||||||
<For
|
|
||||||
each=move || options.get()
|
|
||||||
key=move |item| item.value.clone()
|
|
||||||
children=move |item| {
|
|
||||||
let item = store_value(item);
|
|
||||||
let onclick = move |_| {
|
|
||||||
let SelectOption { value: item_value, label: _ } = item.get_value();
|
|
||||||
value.set(Some(item_value));
|
|
||||||
is_show_menu.set(false);
|
|
||||||
};
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="thaw-select-menu__item"
|
|
||||||
class=(
|
|
||||||
"thaw-select-menu__item-selected",
|
|
||||||
move || value.get() == Some(item.get_value().value),
|
|
||||||
)
|
|
||||||
|
|
||||||
on:click=onclick
|
|
||||||
>
|
|
||||||
{item.get_value().label}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
<Follower slot show=is_show_menu placement=FollowerPlacement::BottomStart width=FollowerWidth::Target>
|
||||||
|
<div
|
||||||
|
class="thaw-select-menu"
|
||||||
|
style=move || menu_css_vars.get()
|
||||||
|
ref=menu_ref
|
||||||
|
>
|
||||||
|
<For
|
||||||
|
each=move || options.get()
|
||||||
|
key=move |item| item.value.clone()
|
||||||
|
children=move |item| {
|
||||||
|
let item = store_value(item);
|
||||||
|
let onclick = move |_| {
|
||||||
|
let SelectOption { value: item_value, label: _ } = item.get_value();
|
||||||
|
value.set(Some(item_value));
|
||||||
|
is_show_menu.set(false);
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="thaw-select-menu__item"
|
||||||
|
class=(
|
||||||
|
"thaw-select-menu__item-selected",
|
||||||
|
move || value.get() == Some(item.get_value().value),
|
||||||
|
)
|
||||||
|
|
||||||
|
on:click=onclick
|
||||||
|
>
|
||||||
|
{item.get_value().label}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Follower>
|
||||||
|
</Binder>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,16 +21,12 @@
|
||||||
background-color: var(--thaw-background-color);
|
background-color: var(--thaw-background-color);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
position: absolute;
|
width: 100%;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
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: 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-select-menu__item {
|
.thaw-select-menu__item {
|
||||||
|
|
|
@ -1,30 +1,36 @@
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
use leptos::*;
|
use leptos::{html::AnyElement, *};
|
||||||
/// https://github.com/solidjs/solid/blob/main/packages/solid/web/src/index.ts#L56
|
/// https://github.com/solidjs/solid/blob/main/packages/solid/web/src/index.ts#L56
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Teleport(
|
pub fn Teleport(
|
||||||
#[prop(into, optional)] mount: Option<web_sys::Element>,
|
#[prop(into, optional)] mount: Option<web_sys::Element>,
|
||||||
children: Children,
|
#[prop(optional, into)] element: Option<HtmlElement<AnyElement>>,
|
||||||
|
#[prop(optional)] children: Option<Children>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
cfg_if! { if #[cfg(target_arch = "wasm32")] {
|
cfg_if! { if #[cfg(target_arch = "wasm32")] {
|
||||||
use leptos::{
|
use leptos::wasm_bindgen::JsCast;
|
||||||
wasm_bindgen::JsCast,
|
|
||||||
leptos_dom::Mountable
|
|
||||||
};
|
|
||||||
let mount = mount.unwrap_or_else(|| {
|
let mount = mount.unwrap_or_else(|| {
|
||||||
document()
|
document()
|
||||||
.body()
|
.body()
|
||||||
.expect("body element not to exist")
|
.expect("body element not to exist")
|
||||||
.unchecked_into()
|
.unchecked_into()
|
||||||
});
|
});
|
||||||
let node = children().into_view();
|
|
||||||
let node = node.get_mountable_node();
|
let render_root = if let Some(element) = element {
|
||||||
_ = mount.append_child(&node);
|
element
|
||||||
|
} else if let Some(children) = children {
|
||||||
|
html::div().child(children()).into_any()
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = mount.append_child(&render_root);
|
||||||
on_cleanup(move || {
|
on_cleanup(move || {
|
||||||
_ = mount.remove_child(&node);
|
_ = mount.remove_child(&render_root);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_ = mount;
|
_ = mount;
|
||||||
|
_ = element;
|
||||||
_ = children;
|
_ = children;
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
50
src/utils/event_listener.rs
Normal file
50
src/utils/event_listener.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use leptos::{html::AnyElement, *};
|
||||||
|
use wasm_bindgen::{prelude::Closure, JsCast};
|
||||||
|
|
||||||
|
pub fn add_event_listener<E: ev::EventDescriptor + 'static>(
|
||||||
|
target: HtmlElement<AnyElement>,
|
||||||
|
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: HtmlElement<AnyElement>,
|
||||||
|
event_name: &str,
|
||||||
|
cb: impl Fn(web_sys::Event) + 'static,
|
||||||
|
) -> EventListenerHandle {
|
||||||
|
fn wel(
|
||||||
|
target: HtmlElement<AnyElement>,
|
||||||
|
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();
|
||||||
|
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