From 6798cf215f0e2ad448c03fe1164a21d54a4ce285 Mon Sep 17 00:00:00 2001 From: luoxiao Date: Thu, 9 Nov 2023 23:11:30 +0800 Subject: [PATCH 1/6] feat: binder --- src/components/binder.rs | 103 +++++++++++++++++++++++++++++++++++++++ src/components/mod.rs | 1 + src/loading_bar/mod.rs | 6 +-- 3 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 src/components/binder.rs diff --git a/src/components/binder.rs b/src/components/binder.rs new file mode 100644 index 0000000..456ac06 --- /dev/null +++ b/src/components/binder.rs @@ -0,0 +1,103 @@ +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/mod.rs b/src/components/mod.rs index 4f92fcf..e0d6b10 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,3 +1,4 @@ +mod binder; mod if_comp; mod option_comp; diff --git a/src/loading_bar/mod.rs b/src/loading_bar/mod.rs index 3db30ec..d205a93 100644 --- a/src/loading_bar/mod.rs +++ b/src/loading_bar/mod.rs @@ -52,7 +52,7 @@ pub(crate) fn LoadingBar(#[prop(optional)] comp_ref: ComponentRef .style("transition", "none") .style("max-width", "0"); _ = loading_bar_ref.offset_width(); - loading_bar_ref + _ = loading_bar_ref .style("transition", "max-width 4s linear") .style("max-width", "80%"); } @@ -66,7 +66,7 @@ pub(crate) fn LoadingBar(#[prop(optional)] comp_ref: ComponentRef }; let finish = Callback::new(move |_| { if let Some(loading_bar_ref) = loading_bar_ref.get_untracked() { - loading_bar_ref + _ = loading_bar_ref .style("background-color", "var(--thaw-background-color)") .style("transition", "max-width 0.5s linear") .style("max-width", "100%"); @@ -83,7 +83,7 @@ pub(crate) fn LoadingBar(#[prop(optional)] comp_ref: ComponentRef .style("max-width", "0"); _ = loading_bar_ref.offset_width(); } - loading_bar_ref + _ = loading_bar_ref .style("background-color", "var(--thaw-background-color-error)") .style("transition", "max-width 0.5s linear") .style("max-width", "100%"); From 6ca2b1de3ca74921a4475e1fa62e4d509bfcdec2 Mon Sep 17 00:00:00 2001 From: luoxiao Date: Fri, 10 Nov 2023 17:30:03 +0800 Subject: [PATCH 2/6] 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::*; From 7b44974533d63dfe4221e75188ecae8743f4d0e6 Mon Sep 17 00:00:00 2001 From: luoxiao Date: Sat, 11 Nov 2023 00:38:31 +0800 Subject: [PATCH 3/6] feat: binder component add resize listener --- src/auto_complete/auto-complete.css | 4 +- src/auto_complete/mod.rs | 138 ++++++++++++---------------- src/components/binder/mod.rs | 98 +++++++++++++------- src/utils/event_listener.rs | 9 +- 4 files changed, 128 insertions(+), 121 deletions(-) diff --git a/src/auto_complete/auto-complete.css b/src/auto_complete/auto-complete.css index 44b192f..da27465 100644 --- a/src/auto_complete/auto-complete.css +++ b/src/auto_complete/auto-complete.css @@ -1,6 +1,5 @@ .thaw-auto-complete__menu { - position: relative; - display: inline-block; + width: 100%; max-height: 200px; padding: 5px; background-color: var(--thaw-background-color); @@ -9,7 +8,6 @@ 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); overflow: auto; - z-index: 2000; } .thaw-auto-complete__menu-item { padding: 6px 5px; diff --git a/src/auto_complete/mod.rs b/src/auto_complete/mod.rs index b3c068b..94eaaed 100644 --- a/src/auto_complete/mod.rs +++ b/src/auto_complete/mod.rs @@ -1,6 +1,11 @@ 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::*; pub use theme::AutoCompleteTheme; @@ -37,93 +42,66 @@ pub fn AutoComplete( let is_show_menu = create_rw_signal(false); let auto_complete_ref = create_node_ref::(); - let auto_complete_menu_ref = create_node_ref::(); 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 |_| { if !is_show_menu.get_untracked() { - show_menu(); + is_show_menu.set(true); } true }; view! { -
- -
- -
- - {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! { -
- {label} -
- } - }) - .collect_view() - }} - + +
+
- + +
+ + {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! { +
+ {label} +
+ } + }) + .collect_view() + }} + +
+
+
} } diff --git a/src/components/binder/mod.rs b/src/components/binder/mod.rs index 13bb582..6e504ee 100644 --- a/src/components/binder/mod.rs +++ b/src/components/binder/mod.rs @@ -5,6 +5,7 @@ use crate::{ }; use leptos::{ html::{AnyElement, ElementDescriptor, ToHtmlElement}, + leptos_dom::helpers::WindowListenerHandle, *, }; @@ -26,32 +27,37 @@ pub fn Binder( 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 scroll_listener = store_value(None::>); + let scrollable_element_handle_vec = store_value::>(vec![]); + let resize_handle = store_value(None::); + 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_and_handle_vec.update_value(|vec| vec.push((cursor, None))); + scrollable_element_vec.push(cursor); } 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 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<()>| { - follower_scroll_listener.update_value(|scroll_listener| { + scroll_listener.update_value(|scroll_listener| { if scroll_listener.is_none() { ensure_scroll_listener(); } @@ -60,22 +66,47 @@ pub fn Binder( }; 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(); - } + scrollable_element_handle_vec.update_value(|vec| { + vec.drain(..).into_iter().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()} - + {follower_children()} } @@ -87,27 +118,26 @@ fn FollowerContainer( target_ref: NodeRef, #[prop(into)] add_scroll_listener: Callback>, #[prop(into)] remove_scroll_listener: Callback<()>, + #[prop(into)] add_resize_listener: Callback>, + #[prop(into)] remove_resize_listener: Callback<()>, children: Children, ) -> impl IntoView { let content_ref = create_node_ref::(); + let content_style = create_rw_signal(String::new()); 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 mut style = String::new(); + style.push_str(&format!("width: {}px;", target_rect.width())); + style.push_str(&format!( + "transform: translateX({}px) translateY({}px);", + target_rect.x(), + target_rect.y() + target_rect.height() + )); + content_style.set(style); }); let is_show = create_memo(move |_| { @@ -121,8 +151,10 @@ fn FollowerContainer( if is_show { 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 }); @@ -135,7 +167,7 @@ fn FollowerContainer( "display: none;" } }> -
+
{children()}
diff --git a/src/utils/event_listener.rs b/src/utils/event_listener.rs index 6b5b8e7..0b9df50 100644 --- a/src/utils/event_listener.rs +++ b/src/utils/event_listener.rs @@ -1,8 +1,8 @@ -use leptos::*; +use leptos::{html::AnyElement, *}; use wasm_bindgen::{prelude::Closure, JsCast}; pub fn add_event_listener( - target: &web_sys::Element, + target: HtmlElement, event: E, cb: impl Fn(E::EventType) + 'static, ) -> EventListenerHandle @@ -29,19 +29,18 @@ impl EventListenerHandle { } fn add_event_listener_untyped( - target: &web_sys::Element, + target: HtmlElement, event_name: &str, cb: impl Fn(web_sys::Event) + 'static, ) -> EventListenerHandle { fn wel( - target: &web_sys::Element, + target: HtmlElement, 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()); })) From f5e6eae5585f7499494f40ddccb0c8b21813722f Mon Sep 17 00:00:00 2001 From: luoxiao Date: Sat, 11 Nov 2023 11:49:47 +0800 Subject: [PATCH 4/6] fix: Teleport component cleanup --- src/components/binder/mod.rs | 25 ++++++++++++------------- src/teleport/mod.rs | 26 ++++++++++++++++---------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/components/binder/mod.rs b/src/components/binder/mod.rs index 6e504ee..c6f36b8 100644 --- a/src/components/binder/mod.rs +++ b/src/components/binder/mod.rs @@ -158,20 +158,19 @@ fn FollowerContainer( } 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! { - -
-
- {children()} -
-
-
+ } } diff --git a/src/teleport/mod.rs b/src/teleport/mod.rs index 777c49b..cfa976e 100644 --- a/src/teleport/mod.rs +++ b/src/teleport/mod.rs @@ -1,30 +1,36 @@ 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 #[component] pub fn Teleport( #[prop(into, optional)] mount: Option, - children: Children, + #[prop(optional, into)] element: Option>, + #[prop(optional)] children: Option, ) -> impl IntoView { cfg_if! { if #[cfg(target_arch = "wasm32")] { - use leptos::{ - wasm_bindgen::JsCast, - leptos_dom::Mountable - }; + use leptos::wasm_bindgen::JsCast; let mount = mount.unwrap_or_else(|| { document() .body() .expect("body element not to exist") .unchecked_into() }); - let node = children().into_view(); - let node = node.get_mountable_node(); - _ = mount.append_child(&node); + + let render_root = if let Some(element) = element { + element + } else if let Some(children) = children { + html::div().child(children()).into_any() + } else { + return; + }; + + _ = mount.append_child(&render_root); on_cleanup(move || { - _ = mount.remove_child(&node); + _ = mount.remove_child(&render_root); }); } else { _ = mount; + _ = element; _ = children; }} } From c57910948fc58690f0e37f330f8f72c29257ddb4 Mon Sep 17 00:00:00 2001 From: luoxiao Date: Sun, 12 Nov 2023 01:58:26 +0800 Subject: [PATCH 5/6] feat: binder component add placement --- src/auto_complete/mod.rs | 4 +- src/components/binder/get_placement_style.rs | 65 ++++++++++++++++++++ src/components/binder/mod.rs | 32 +++++++--- 3 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 src/components/binder/get_placement_style.rs diff --git a/src/auto_complete/mod.rs b/src/auto_complete/mod.rs index 94eaaed..8d6fa83 100644 --- a/src/auto_complete/mod.rs +++ b/src/auto_complete/mod.rs @@ -1,7 +1,7 @@ mod theme; use crate::{ - components::{Binder, Follower}, + components::{Binder, Follower, FollowerPlacement}, mount_style, use_theme, utils::StoredMaybeSignal, Input, Theme, @@ -61,7 +61,7 @@ pub fn AutoComplete( allow_value />
- +
Option { + // 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 mut top = target_y + target_height; + + let Some(inner_height) = window_inner_height() else { + return None; + }; + + if top + follower_height > inner_height { + if target_y - follower_height >= 0.0 { + top = target_y - follower_height + } + } + + top + }; + + style.push_str(&format!( + "transform: translateX({left}px) translateY({top}px);" + )); + + Some(style) +} + +fn window_inner_height() -> Option { + let Ok(inner_height) = window().inner_height() else { + return None; + }; + let Some(inner_height) = inner_height.as_f64() else { + return None; + }; + Some(inner_height) +} diff --git a/src/components/binder/mod.rs b/src/components/binder/mod.rs index c6f36b8..15990d9 100644 --- a/src/components/binder/mod.rs +++ b/src/components/binder/mod.rs @@ -1,8 +1,12 @@ +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, @@ -13,6 +17,7 @@ use leptos::{ pub struct Follower { #[prop(into)] show: MaybeSignal, + placement: FollowerPlacement, children: Children, } @@ -25,6 +30,7 @@ pub fn Binder( mount_style("binder", include_str!("./binder.css")); let Follower { show: follower_show, + placement: follower_placement, children: follower_children, } = follower; @@ -102,6 +108,7 @@ pub fn Binder( ( fn FollowerContainer( show: MaybeSignal, target_ref: NodeRef, + placement: FollowerPlacement, #[prop(into)] add_scroll_listener: Callback>, #[prop(into)] remove_scroll_listener: Callback<()>, #[prop(into)] add_resize_listener: Callback>, @@ -125,18 +133,24 @@ fn FollowerContainer( let content_ref = create_node_ref::(); let content_style = create_rw_signal(String::new()); let sync_position: Callback<()> = Callback::new(move |_| { - let Some(target_ref) = target_ref.get().map(|target| target.into_any()) else { + 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(); style.push_str(&format!("width: {}px;", target_rect.width())); - style.push_str(&format!( - "transform: translateX({}px) translateY({}px);", - target_rect.x(), - target_rect.y() + target_rect.height() - )); + 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); }); @@ -149,7 +163,9 @@ fn FollowerContainer( } let is_show = show.get(); if is_show { - sync_position.call(()); + request_animation_frame(move || { + sync_position.call(()); + }); add_scroll_listener.call(sync_position); add_resize_listener.call(sync_position); } else { From 012ab684bad7669e48b53d2eac79e2c771c1d9f8 Mon Sep 17 00:00:00 2001 From: luoxiao Date: Sun, 12 Nov 2023 02:34:40 +0800 Subject: [PATCH 6/6] feat: binder component add width --- src/auto_complete/mod.rs | 4 +- src/color_picker/color-picker.css | 5 - src/color_picker/mod.rs | 50 ++++------ src/components/binder/get_placement_style.rs | 12 +-- src/components/binder/mod.rs | 23 ++++- src/select/mod.rs | 97 +++++++++----------- src/select/select.css | 6 +- 7 files changed, 91 insertions(+), 106 deletions(-) diff --git a/src/auto_complete/mod.rs b/src/auto_complete/mod.rs index 8d6fa83..998d54d 100644 --- a/src/auto_complete/mod.rs +++ b/src/auto_complete/mod.rs @@ -1,7 +1,7 @@ mod theme; use crate::{ - components::{Binder, Follower, FollowerPlacement}, + components::{Binder, Follower, FollowerPlacement, FollowerWidth}, mount_style, use_theme, utils::StoredMaybeSignal, Input, Theme, @@ -61,7 +61,7 @@ pub fn AutoComplete( allow_value />
- +
) -> impl IntoVi let trigger_ref = create_node_ref::(); let popover_ref = create_node_ref::(); let show_popover = move |_| { - let rect = trigger_ref.get().unwrap().get_bounding_client_rect(); 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 el = ev.target(); @@ -96,28 +86,24 @@ pub fn ColorPicker(#[prop(optional, into)] value: RwSignal) -> impl IntoVi on_cleanup(move || timer.remove()); view! { -
-
- {move || label.get()} + +
+
+ {move || label.get()} +
-
- -
+ +
- - -
- + + +
+ + } } diff --git a/src/components/binder/get_placement_style.rs b/src/components/binder/get_placement_style.rs index 79ab2df..58554d4 100644 --- a/src/components/binder/get_placement_style.rs +++ b/src/components/binder/get_placement_style.rs @@ -32,19 +32,17 @@ pub fn get_follower_placement_style( let follower_height = follower_rect.height(); let target_y = target_rect.y(); let target_height = target_rect.height(); - let mut top = target_y + target_height; + let top = target_y + target_height; let Some(inner_height) = window_inner_height() else { return None; }; - if top + follower_height > inner_height { - if target_y - follower_height >= 0.0 { - top = target_y - follower_height - } + if top + follower_height > inner_height && target_y - follower_height >= 0.0 { + target_y - follower_height + } else { + top } - - top }; style.push_str(&format!( diff --git a/src/components/binder/mod.rs b/src/components/binder/mod.rs index 15990d9..56ba572 100644 --- a/src/components/binder/mod.rs +++ b/src/components/binder/mod.rs @@ -17,10 +17,20 @@ use leptos::{ pub struct Follower { #[prop(into)] show: MaybeSignal, + #[prop(optional)] + width: Option, placement: FollowerPlacement, children: Children, } +#[derive(Clone)] +pub enum FollowerWidth { + Target, + Px(u32), +} + +impl Copy for FollowerWidth {} + #[component] pub fn Binder( #[prop(into)] target_ref: NodeRef, @@ -30,6 +40,7 @@ pub fn Binder( mount_style("binder", include_str!("./binder.css")); let Follower { show: follower_show, + width: follower_width, placement: follower_placement, children: follower_children, } = follower; @@ -73,7 +84,7 @@ pub fn Binder( let remove_scroll_listener = move |_| { scrollable_element_handle_vec.update_value(|vec| { - vec.drain(..).into_iter().for_each(|handle| handle.remove()); + vec.drain(..).for_each(|handle| handle.remove()); }); scroll_listener.set_value(None); }; @@ -108,6 +119,7 @@ pub fn Binder( ( fn FollowerContainer( show: MaybeSignal, target_ref: NodeRef, + width: Option, placement: FollowerPlacement, #[prop(into)] add_scroll_listener: Callback>, #[prop(into)] remove_scroll_listener: Callback<()>, @@ -142,7 +155,13 @@ fn FollowerContainer( let target_rect = target_ref.get_bounding_client_rect(); let content_rect = content_ref.get_bounding_client_rect(); let mut style = String::new(); - style.push_str(&format!("width: {}px;", target_rect.width())); + 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) { diff --git a/src/select/mod.rs b/src/select/mod.rs index 4aa86df..e5f329c 100644 --- a/src/select/mod.rs +++ b/src/select/mod.rs @@ -1,6 +1,11 @@ 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::*; use std::hash::Hash; @@ -66,20 +71,7 @@ where let trigger_ref = create_node_ref::(); let menu_ref = create_node_ref::(); let show_menu = move |_| { - let rect = trigger_ref.get().unwrap().get_bounding_client_rect(); 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 el = ev.target(); @@ -111,47 +103,46 @@ where None => String::new(), }); view! { -
+ +
- {move || select_option_label.get()} - -
- -
- - {item.get_value().label} -
- } - } - /> + {move || select_option_label.get()}
-
+ +
+ + {item.get_value().label} +
+ } + } + /> + +
+ + } } diff --git a/src/select/select.css b/src/select/select.css index ac69e62..702454f 100644 --- a/src/select/select.css +++ b/src/select/select.css @@ -21,16 +21,12 @@ background-color: var(--thaw-background-color); box-sizing: border-box; padding: 5px; - position: absolute; - top: 0; - left: 0; - right: 0; + width: 100%; max-height: 200px; border-radius: 3px; 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); overflow: auto; - z-index: 1000; } .thaw-select-menu__item {