flams_web_utils/components/binder.rs
1/*
2#![allow(clippy::module_name_repetitions)]
3#![allow(clippy::too_many_lines)]
4
5use leptos::prelude::*;
6use thaw_utils::{add_event_listener, get_scroll_parent_node};
7use super::popover::DivOrMrowRef;
8use thaw_components::{Follower, FollowerPlacement, FollowerWidth, Teleport};
9use leptos::web_sys::DomRect;
10
11#[component]
12pub fn Binder<Ch:IntoView+'static>(
13 /// Used to track DOM locations
14 #[prop(into)]
15 target_ref: DivOrMrowRef,
16 #[prop(optional)] mut max_width:u32,
17 /// Content for pop-up display
18 follower: Follower,
19 children: TypedChildren<Ch>,
20) -> impl IntoView {
21 let children = children.into_inner();
22 if max_width == 0 { max_width = 600 };
23 crate::inject_css("thaw-id-binder", include_str!("./binder.css"));
24 let Follower {
25 show: follower_show,
26 width: follower_width,
27 placement: follower_placement,
28 children: follower_children,
29 } = follower;
30
31 let scrollable_element_handle_vec = StoredValue::<Vec<thaw_utils::EventListenerHandle>>::new(vec![]);
32 let resize_handle = StoredValue::new(None::<WindowListenerHandle>);
33 let follower_ref = NodeRef::<leptos::html::Div>::new();
34 let content_ref = NodeRef::<leptos::html::Div>::new();
35 let content_style = RwSignal::new(String::new());
36 let placement_str = RwSignal::new(follower_placement.as_str());
37 let sync_position = move || {
38 let Some(follower_el) = follower_ref.get_untracked() else {
39 return;
40 };
41 let Some(content_ref) = content_ref.get_untracked() else {
42 return;
43 };
44 let Some(target_ref) = target_ref.get_untracked() else {
45 return;
46 };
47 let follower_rect = follower_el.get_bounding_client_rect();
48 let target_rect = target_ref.get_bounding_client_rect();
49 let content_rect = content_ref.get_bounding_client_rect();
50 let mut style = format!("max-width:{max_width}px;");
51 if let Some(width) = follower_width {
52 let width = match width {
53 FollowerWidth::Target => format!("width: {}px;", target_rect.width()),
54 FollowerWidth::MinTarget => format!("min-width: {}px;", target_rect.width()),
55 FollowerWidth::Px(width) => format!("width: {width}px;"),
56 };
57 style.push_str(&width);
58 }
59 if let Some(FollowerPlacementOffset {
60 top,
61 left,
62 transform,
63 placement,
64 }) = get_follower_placement_offset(
65 max_width,
66 follower_placement,
67 target_rect,
68 follower_rect,
69 content_rect,
70 ) {
71 placement_str.set(placement.as_str());
72 style.push_str(&format!(
73 "transform-origin: {};",
74 placement.transform_origin()
75 ));
76 style.push_str(&format!(
77 "transform: translateX({left}px) translateY({top}px) {transform};"
78 ));
79 } else {
80 leptos::logging::error!("Thaw-Binder: get_follower_placement_style return None");
81 }
82
83 content_style.set(style);
84 };
85
86 let ensure_listener = move || {
87 let target_ref = target_ref.get_untracked();
88 let Some(el) = target_ref.as_deref() else {
89 return;
90 };
91
92 let mut handle_vec = vec![];
93 let mut cursor = get_scroll_parent_node(el);
94 while let Some(node) = cursor.take() {
95 cursor = get_scroll_parent_node(&node);
96
97 let handle = add_event_listener(node, leptos::ev::scroll, move |_| {
98 sync_position();
99 });
100 handle_vec.push(handle);
101 }
102 scrollable_element_handle_vec.set_value(handle_vec);
103
104 resize_handle.update_value(move |resize_handle| {
105 if let Some(handle) = resize_handle.take() {
106 handle.remove();
107 }
108 let handle = window_event_listener(leptos::ev::resize, move |_| {
109 sync_position();
110 });
111 *resize_handle = Some(handle);
112 });
113 };
114
115 let remove_listener = move || {
116 scrollable_element_handle_vec.update_value(|vec| {
117 vec.drain(..).for_each(thaw_utils::EventListenerHandle::remove);
118 });
119 resize_handle.update_value(move |handle| {
120 if let Some(handle) = handle.take() {
121 handle.remove();
122 }
123 });
124 };
125
126 Effect::new(move |_| {
127 if target_ref.get().is_none() {
128 return;
129 }
130 if content_ref.get().is_none() {
131 return;
132 }
133 if follower_show.get() {
134 request_animation_frame(move || {
135 sync_position();
136 });
137
138 remove_listener();
139 ensure_listener();
140 } else {
141 remove_listener();
142 }
143 });
144
145 Owner::on_cleanup(move || {
146 remove_listener();
147 });
148
149 //let follower_injection = FollowerInjection(Callback::new(move |()| sync_position()));
150
151 view! {
152 {children()}
153 <Teleport immediate=follower_show>
154 <div class="thaw-binder-follower" node_ref=follower_ref>
155 <div
156 class="thaw-binder-follower-content"
157 data-thaw-placement=move || placement_str.get()
158 node_ref=content_ref
159 style=move || content_style.get()
160 >
161 //<Provider value=follower_injection>
162 {follower_children()}
163 //</Provider>
164 </div>
165 </div>
166 </Teleport>
167 }
168}
169/*
170#[derive(Debug, Clone, Copy)]
171pub struct FollowerInjection(Callback<()>);
172
173impl FollowerInjection {
174 pub fn expect_context() -> Self {
175 expect_context()
176 }
177
178 pub fn refresh_position(&self) {
179 self.0.run(());
180 }
181}
182 */
183
184struct FollowerPlacementOffset {
185 pub top: f64,
186 pub left: f64,
187 pub transform: String,
188 pub placement: FollowerPlacement,
189}
190
191#[allow(clippy::cognitive_complexity)]
192#[allow(clippy::needless_pass_by_value)]
193#[allow(clippy::cast_lossless)]
194fn get_follower_placement_offset(
195 max_width:u32,
196 placement: FollowerPlacement,
197 target_rect: DomRect,
198 follower_rect: DomRect,
199 content_rect: DomRect,
200) -> Option<FollowerPlacementOffset> {
201 let barrier_left = (max_width / 2) as f64;
202 let barrier_right = window_inner_width().map(|w| w - barrier_left)?;
203 let (left, placement, top, transform) = match placement {
204 FollowerPlacement::Top | FollowerPlacement::TopStart | FollowerPlacement::TopEnd => {
205 let window_inner_height = window_inner_height()?;
206 let content_height = content_rect.height();
207 let target_top = target_rect.top();
208 let target_bottom = target_rect.bottom();
209 let top = target_top - content_height;
210 let (top, new_placement) =
211 if top < 0.0 && target_bottom + content_height <= window_inner_height {
212 let new_placement = if placement == FollowerPlacement::Top {
213 FollowerPlacement::Bottom
214 } else if placement == FollowerPlacement::TopStart {
215 FollowerPlacement::BottomStart
216 } else if placement == FollowerPlacement::TopEnd {
217 FollowerPlacement::BottomEnd
218 } else {
219 unreachable!()
220 };
221 (target_bottom, new_placement)
222 } else {
223 (top, placement)
224 };
225
226 if placement == FollowerPlacement::Top {
227 let left = (target_rect.left() + target_rect.width() / 2.0).max(barrier_left).min(barrier_right);
228 //leptos::logging::log!("Here: {left} {top}");
229 let transform = String::from("translateX(-50%)");
230 (left, new_placement, top, transform)
231 } else if placement == FollowerPlacement::TopStart {
232 let left = target_rect.left().max(barrier_left).min(barrier_right);
233 //leptos::logging::log!("Here: {left} {top}");
234 let transform = String::new();
235 (left, new_placement, top, transform)
236 } else if placement == FollowerPlacement::TopEnd {
237 let left = target_rect.right().max(barrier_left).min(barrier_right);
238 //leptos::logging::log!("Here: {left} {top}");
239 let transform = String::from("translateX(-100%)");
240 (left, new_placement, top, transform)
241 } else {
242 unreachable!()
243 }
244 }
245 FollowerPlacement::Bottom
246 | FollowerPlacement::BottomStart
247 | FollowerPlacement::BottomEnd => {
248 let window_inner_height = window_inner_height()?;
249 let content_height = content_rect.height();
250 let target_top = target_rect.top();
251 let target_bottom = target_rect.bottom();
252 let top = target_bottom;
253 let (top, new_placement) = if top + content_height > window_inner_height
254 && target_top - content_height >= 0.0
255 {
256 let new_placement = if placement == FollowerPlacement::Bottom {
257 FollowerPlacement::Top
258 } else if placement == FollowerPlacement::BottomStart {
259 FollowerPlacement::TopStart
260 } else if placement == FollowerPlacement::BottomEnd {
261 FollowerPlacement::TopEnd
262 } else {
263 unreachable!()
264 };
265 (target_top - content_height, new_placement)
266 } else {
267 (top, placement)
268 };
269 if placement == FollowerPlacement::Bottom {
270 let left = (target_rect.left() + target_rect.width() / 2.0).max(barrier_left).min(barrier_right);
271 let transform = String::from("translateX(-50%)");
272 (left, new_placement, top, transform)
273 } else if placement == FollowerPlacement::BottomStart {
274 let left = target_rect.left().max(barrier_left).min(barrier_right);
275 let transform = String::new();
276 (left, new_placement, top, transform)
277 } else if placement == FollowerPlacement::BottomEnd {
278 let left = target_rect.right().max(barrier_left).min(barrier_right);
279 let transform = String::from("translateX(-100%)");
280 (left, new_placement, top, transform)
281 } else {
282 unreachable!()
283 }
284 }
285 FollowerPlacement::Left | FollowerPlacement::LeftStart | FollowerPlacement::LeftEnd => {
286 let window_inner_width = window_inner_width()?;
287 let content_width = content_rect.width();
288 let target_left = target_rect.left();
289 let target_right = target_rect.right();
290 let left = target_left - content_width;
291
292 let (left, new_placement) =
293 if left < 0.0 && target_right + content_width <= window_inner_width {
294 let new_placement = if placement == FollowerPlacement::Left {
295 FollowerPlacement::Right
296 } else if placement == FollowerPlacement::LeftStart {
297 FollowerPlacement::RightStart
298 } else if placement == FollowerPlacement::LeftEnd {
299 FollowerPlacement::RightEnd
300 } else {
301 unreachable!()
302 };
303 (target_right, new_placement)
304 } else {
305 (left, placement)
306 };
307 if placement == FollowerPlacement::Left {
308 let top = target_rect.top() + target_rect.height() / 2.0;
309 let transform = String::from("translateY(-50%)");
310 (left, new_placement, top, transform)
311 } else if placement == FollowerPlacement::LeftStart {
312 let top = target_rect.top();
313 let transform = String::new();
314 (left, new_placement, top, transform)
315 } else if placement == FollowerPlacement::LeftEnd {
316 let top = target_rect.bottom();
317 let transform = String::from("translateY(-100%)");
318 (left, new_placement, top, transform)
319 } else {
320 unreachable!()
321 }
322 }
323 FollowerPlacement::Right | FollowerPlacement::RightStart | FollowerPlacement::RightEnd => {
324 let window_inner_width = window_inner_width()?;
325 let content_width = content_rect.width();
326 let target_left = target_rect.left();
327 let target_right = target_rect.right();
328 let left = target_right;
329 let (left, new_placement) = if left + content_width > window_inner_width
330 && target_left - content_width >= 0.0
331 {
332 let new_placement = if placement == FollowerPlacement::Right {
333 FollowerPlacement::Left
334 } else if placement == FollowerPlacement::RightStart {
335 FollowerPlacement::LeftStart
336 } else if placement == FollowerPlacement::RightEnd {
337 FollowerPlacement::LeftEnd
338 } else {
339 unreachable!()
340 };
341 (target_left - content_width, new_placement)
342 } else {
343 (left, placement)
344 };
345
346 if placement == FollowerPlacement::Right {
347 let top = target_rect.top() + target_rect.height() / 2.0;
348 let transform = String::from("translateY(-50%)");
349 (left, new_placement, top, transform)
350 } else if placement == FollowerPlacement::RightStart {
351 let top = target_rect.top();
352 let transform = String::new();
353 (left, new_placement, top, transform)
354 } else if placement == FollowerPlacement::RightEnd {
355 let top = target_rect.bottom();
356 let transform = String::from("translateY(-100%)");
357 (left, new_placement, top, transform)
358 } else {
359 unreachable!()
360 }
361 }
362 };
363
364 Some(FollowerPlacementOffset {
365 top: top - follower_rect.top(),
366 left: left - follower_rect.left(),
367 placement,
368 transform,
369 })
370}
371
372fn window_inner_width() -> Option<f64> {
373 let inner_width = window().inner_width().ok()?;
374 let inner_width = inner_width.as_f64()?;
375 Some(inner_width)
376}
377
378fn window_inner_height() -> Option<f64> {
379 let inner_height = window().inner_height().ok()?;
380 let inner_height = inner_height.as_f64()?;
381 Some(inner_height)
382}
383 */