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 */