flams_web_utils/components/
popover.rs

1use leptos::prelude::*;
2use thaw::{
3    Dialog, DialogSurface, Popover as ThawPopover, PopoverAppearance, PopoverPosition,
4    PopoverProps as ThawPopoverProps,
5};
6use thaw_utils::BoxCallback;
7
8pub use thaw::{PopoverSize, PopoverTrigger, PopoverTriggerType};
9
10#[slot]
11pub struct OnClickModal {
12    children: Children,
13    #[prop(optional, into)]
14    signal: RwSignal<bool>,
15}
16
17#[component]
18pub fn Popover(
19    #[prop(optional, into)] class: MaybeProp<String>,
20    /// Action that displays the popover.
21    #[prop(optional)]
22    trigger_type: PopoverTriggerType,
23    /// The element or component that triggers popover.
24    popover_trigger: PopoverTrigger<AnyView>,
25    /// Configures the position of the Popover.
26    #[prop(optional,default=PopoverPosition::Top)]
27    position: PopoverPosition,
28    #[prop(optional)] on_click_modal: Option<OnClickModal>,
29    #[prop(optional)] on_click_signal: Option<RwSignal<bool>>,
30    children: Children,
31    #[prop(optional, into)] appearance: MaybeProp<PopoverAppearance>,
32    #[prop(optional, into)] size: Signal<PopoverSize>,
33    #[prop(optional, into)] on_open: Option<BoxCallback>,
34    #[prop(optional, into)] on_close: Option<BoxCallback>,
35) -> impl IntoView {
36    let trigger = popover_trigger.children.into_inner();
37
38    let modal_signal = on_click_modal
39        .as_ref()
40        .map(|OnClickModal { signal, .. }| *signal)
41        .or(on_click_signal);
42    ThawPopover(ThawPopoverProps {
43        class,
44        trigger_type,
45        position,
46        appearance,
47        size,
48        on_open,
49        on_close,
50        popover_trigger: PopoverTrigger {
51            children: leptos::children::ToChildren::to_children(move || {
52                let t = trigger();
53                if let Some(sig) = modal_signal {
54                    leptos::either::Either::Left(t.add_any_attr(leptos::tachys::html::event::on(
55                        leptos::tachys::html::event::click,
56                        Box::new(move |_| sig.set(true)),
57                    )))
58                } else {
59                    leptos::either::Either::Right(t)
60                }
61            }),
62        },
63        children: Box::new(move || {
64            if let Some(OnClickModal { signal, children }) = on_click_modal {
65                view! {
66                    <Dialog open=signal>
67                        <DialogSurface>//<DialogBody>
68                            {children()}
69                        /*</DialogBody>*/</DialogSurface>
70                    </Dialog>
71                }
72                .into_any()
73            } else {
74                children().into_any()
75            }
76        }),
77    }).into_any()
78}
79
80/*
81#![allow(clippy::module_name_repetitions)]
82#![allow(clippy::too_many_lines)]
83
84use leptos::either::Either;
85use leptos::prelude::*;
86use leptos::{ev, html};
87use std::time::Duration;
88use thaw_utils::{add_event_listener, class_list,BoxCallback};
89use thaw_components::{CSSTransition,Follower};
90use super::binder::Binder;
91use thaw::{Dialog,DialogSurface};
92
93#[slot]
94pub struct OnClickModal {
95    children:Children,
96    #[prop(optional, into)] signal:RwSignal<bool>
97}
98
99/// Largely copied from [thaw](https://docs.rs/thaw), but modified
100/// to work with MathML.
101#[component]
102pub fn Popover<Ch:IntoView+'static,T:IntoView+'static>(
103    #[prop(optional, into)] class: MaybeProp<String>,
104    /// Action that displays the popover.
105    #[prop(optional)]
106    trigger_type: PopoverTriggerType,
107    /// The element or component that triggers popover.
108    popover_trigger: PopoverTrigger<T>,
109    /// Configures the position of the Popover.
110    #[prop(optional)]
111    position: PopoverPosition,
112    #[prop(optional)] max_width:u32,
113    #[prop(optional)]
114    on_click_modal:Option<OnClickModal>,
115    #[prop(optional)]
116    on_click_signal:Option<RwSignal<bool>>,
117    children: TypedChildren<Ch>,
118    #[prop(optional, into)]
119    appearance: MaybeProp<PopoverAppearance>,
120    #[prop(optional, into)] size: Signal<PopoverSize>,
121    #[prop(optional, into)] on_open: Option<BoxCallback>,
122    #[prop(optional, into)] on_close: Option<BoxCallback>,
123    #[prop(optional)] node_type: DivOrMrow,
124) -> impl IntoView {
125    //#[derive(Copy,Clone)]
126    //struct InnerPopover(RwSignal<Option<RwSignal<bool>>>);
127    //let previous_popover = use_context::<InnerPopover>();
128    //let this_popover = InnerPopover(RwSignal::new(None));
129    let children = children.into_inner();
130
131    crate::inject_css("thaw-id-popover", include_str!("./popover.css"));
132    let config_provider = thaw::ConfigInjection::expect_context();
133    let popover_ref = NodeRef::<html::Div>::new();
134    let target_ref = node_type.new_ref();
135    let is_show_popover = RwSignal::new(false);
136    let show_popover_handle = StoredValue::new(None::<TimeoutHandle>);
137
138    if on_open.is_some() || on_close.is_some() {
139        Effect::watch(
140            move || is_show_popover.get(),
141            move |is_shown, prev_is_shown, _| {
142                if prev_is_shown != Some(is_shown) {
143                    if *is_shown {
144                        if let Some(on_open) = &on_open {
145                            on_open();
146                        }
147                    } else if let Some(on_close) = &on_close {
148                        on_close();
149                    }
150                }
151            },
152            false,
153        );
154    }
155
156    let on_mouse_enter = move |_| {
157        if trigger_type != PopoverTriggerType::Hover {
158            return;
159        }
160        show_popover_handle.update_value(|handle| {
161            if let Some(handle) = handle.take() {
162                handle.clear();
163            }
164        });
165        is_show_popover.set(true);
166    };
167    let on_mouse_leave = move |_| {
168        if trigger_type != PopoverTriggerType::Hover {
169            return;
170        }
171        show_popover_handle.update_value(|handle| {
172            if let Some(handle) = handle.take() {
173                handle.clear();
174            }
175            *handle = set_timeout_with_handle(
176                move || {
177                    is_show_popover.set(false);
178                },
179                Duration::from_millis(100),
180            )
181            .ok();
182        });
183    };
184    #[cfg(any(feature = "csr", feature = "hydrate"))]
185    {
186        let handle = window_event_listener(ev::click, move |ev| {
187            use leptos::wasm_bindgen::__rt::IntoJsResult;
188            if trigger_type != PopoverTriggerType::Click {
189                return;
190            }
191            if !is_show_popover.get_untracked() {
192                return;
193            }
194            let el = ev.target();
195            let mut el: Option<leptos::web_sys::Element> =
196                el.into_js_result().map_or(None, |el| Some(el.into()));
197            let body = document().body().expect("No document found!");
198            while let Some(current_el) = el {
199                if current_el == *body {
200                    break;
201                };
202                let Some(popover_el) = popover_ref.get_untracked() else {
203                    break;
204                };
205                if current_el == **popover_el {
206                    return;
207                }
208                el = current_el.parent_element();
209            }
210            is_show_popover.set(false);
211        });
212        on_cleanup(move || handle.remove());
213    }
214    Effect::new(move |_| {
215        let Some(target_el) = target_ref.get() else {
216            return;
217        };
218        let handler = add_event_listener(target_el, ev::click, move |event| {
219            if trigger_type != PopoverTriggerType::Click {
220                return;
221            }
222            event.stop_propagation();
223            is_show_popover.update(|show| *show = !*show);
224        });
225        on_cleanup(move || handler.remove());
226    });
227
228    let PopoverTrigger {
229        class: trigger_class,
230        children: trigger_children,
231    } = popover_trigger;
232    let trigger_children = trigger_children.into_inner();
233
234    let modal_signal = on_click_modal.as_ref().map(|OnClickModal{signal,..}| *signal)
235        .or(on_click_signal);
236
237    let do_trigger = move || match target_ref {
238        DivOrMrowRef::Div(target_ref) => Either::Left(view!{
239            <div
240                class=class_list![
241                    "thaw-popover-trigger",
242                    move || is_show_popover.get().then(|| "thaw-popover-trigger--open".to_string()),
243                    trigger_class
244                ]
245                node_ref=target_ref
246                on:click=move |_| if let Some(s) = modal_signal { s.set(true) }
247                on:mouseenter=on_mouse_enter
248                on:mouseleave=on_mouse_leave
249            >
250                {trigger_children()}
251            </div>
252        }),
253        DivOrMrowRef::Mrow(target_ref) => Either::Right(view!{
254            <mrow
255                class=class_list![
256                    "thaw-popover-trigger",
257                    move || is_show_popover.get().then(|| "thaw-popover-trigger--open".to_string()),
258                    trigger_class
259                ]
260                node_ref=target_ref
261                on:click=move |_| if let Some(s) = modal_signal { s.set(true) }
262                on:mouseenter=on_mouse_enter
263                on:mouseleave=on_mouse_leave
264            >
265                {trigger_children()}
266            </mrow>
267        })
268    };
269
270    view! {
271        {on_click_modal.map(|OnClickModal{signal,children}| view!{
272            <Dialog open=signal>
273                <DialogSurface>//<DialogBody>
274                    {children()}
275                /*</DialogBody>*/</DialogSurface>
276            </Dialog>
277        })}
278        <Binder target_ref max_width>
279            {do_trigger()}
280            <Follower slot show=is_show_popover placement=position>
281                <CSSTransition
282                    //node_ref=popover_ref
283                    name="popover-transition"
284                    appear=is_show_popover.get_untracked()
285                    show=is_show_popover
286                    node_ref=popover_ref // remove
287                    let:display
288                >
289                    <div
290                        class=class_list![
291                            "thaw-config-provider thaw-popover-surface",
292                            move || format!("thaw-popover-surface--{}", size.get().as_str()),
293                            move || appearance.get().map(|a| format!("thaw-popover-surface--{}", a.as_str())),
294                            class
295                        ]
296                        data-thaw-id=config_provider.id()
297                        style=move || display.get().unwrap_or_default()
298
299                        node_ref=popover_ref
300                        on:mouseenter=on_mouse_enter
301                        on:mouseleave=on_mouse_leave
302                    >
303                        {children()}
304                        <div class="thaw-popover-surface__angle"></div>
305                    </div>
306                </CSSTransition>
307            </Follower>
308        </Binder>
309    }
310}
311
312#[derive(Debug, Default, Clone)]
313pub enum PopoverSize {
314    Small,
315    #[default]
316    Medium,
317    Large,
318}
319
320impl PopoverSize {
321    pub const fn as_str(&self) -> &'static str {
322        match self {
323            Self::Small => "small",
324            Self::Medium => "medium",
325            Self::Large => "large",
326        }
327    }
328}
329
330#[derive(Debug, Copy, Clone, Hash, Default)]
331pub enum DivOrMrow {
332    #[default]
333    Div,
334    Mrow,
335}
336impl DivOrMrow {
337    #[inline]
338    fn new_ref(self) -> DivOrMrowRef {
339        match self {
340            Self::Div => DivOrMrowRef::Div(NodeRef::new()),
341            Self::Mrow => DivOrMrowRef::Mrow(NodeRef::new()),
342        }
343    }
344}
345
346#[derive(Debug, Copy, Clone)]
347pub enum DivOrMrowRef {
348    Div(NodeRef::<html::Div>),
349    Mrow(NodeRef::<leptos::tachys::mathml::Mrow>)
350}
351impl DivOrMrowRef {
352    #[inline]
353    pub fn get(&self) -> Option<DivOrMrowElem> {
354        match self {
355            Self::Div(r) => r.get().map(DivOrMrowElem::Div),
356            Self::Mrow(r) => r.get().map(DivOrMrowElem::Mrow)
357        }
358    }
359    #[inline]
360    pub fn get_untracked(&self) -> Option<DivOrMrowElem> {
361        match self {
362            Self::Div(r) => r.get_untracked().map(DivOrMrowElem::Div),
363            Self::Mrow(r) => r.get_untracked().map(DivOrMrowElem::Mrow)
364        }
365    }
366}
367
368pub enum DivOrMrowElem {
369    Div(leptos::web_sys::HtmlDivElement),
370    Mrow(leptos::web_sys::Element)
371}
372impl std::ops::Deref for DivOrMrowElem {
373    type Target = leptos::web_sys::Element;
374    #[inline]
375    fn deref(&self) -> &Self::Target {
376        match self {
377            Self::Div(e) => e,
378            Self::Mrow(e) => e
379        }
380    }
381}
382impl DivOrMrowElem {
383    #[inline]
384    pub fn get_bounding_client_rect(&self) -> leptos::web_sys::DomRect {
385        match self {
386            Self::Div(r) => r.get_bounding_client_rect(),
387            Self::Mrow(r) => r.get_bounding_client_rect()
388        }
389    }
390}
391impl From<DivOrMrowElem> for leptos::web_sys::EventTarget {
392    #[inline]
393    fn from(value: DivOrMrowElem) -> Self {
394        match value {
395            DivOrMrowElem::Div(e) => e.into(),
396            DivOrMrowElem::Mrow(e) => e.into()
397        }
398    }
399}
400
401#[slot]
402pub struct PopoverTrigger<Ch> {
403    #[prop(optional, into)]
404    class: MaybeProp<String>,
405    children: TypedChildren<Ch>,
406}
407
408pub use thaw::{PopoverPosition,PopoverAppearance};
409
410#[derive(Default, PartialEq, Eq, Clone, Copy)]
411pub enum PopoverTriggerType {
412    #[default]
413    Hover,
414    Click,
415}
416 */