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