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