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