use std::rc::Rc;
use dioxus::{
document::{Eval, eval},
prelude::*,
};
use dioxus::{html::geometry::ClientPoint, logger::tracing};
mod floating;
mod scrollable_view;
pub use floating::{Floating, FloatingOptions, Middleware, OffsetOptions, Placement, ScrollState};
pub use scrollable_view::{ScrollableContext, ScrollableView};
pub fn use_floating() -> Floating {
use_hook(Floating::default)
}
pub fn use_scroll_context() -> ScrollableContext {
use_context::<ScrollableContext>()
}
pub fn use_scroll_state() -> Signal<Option<ScrollState>> {
let ctx = use_scroll_context();
ctx.scroll_state
}
pub fn use_scrollable_ref() -> Signal<Option<Rc<MountedData>>> {
let ctx = use_scroll_context();
ctx.scrollable_ref
}
pub fn use_click_outside(
target_element_id: ReadSignal<String>,
on_click_outside: EventHandler<()>,
) {
let mut eval_handle = use_signal(|| Option::<Eval>::None);
use_effect(move || {
let element_id = target_element_id();
spawn(async move {
let mut eval = eval(&format!(
r#"
const elementId = "{}";
const controller = new AbortController();
window.addEventListener("click", (event) => {{
const element = document.getElementById(elementId);
if (element && !element.contains(event.target)) {{
dioxus.send("outside");
}}
}}, {{ capture: true, signal: controller.signal }});
await dioxus.recv();
controller.abort();
"#,
element_id
));
if let Some(old_eval) = eval_handle() {
let _ = old_eval.send("drop");
}
eval_handle.set(Some(eval.clone()));
while let Ok(message) = eval.recv::<String>().await {
if message == "outside" {
on_click_outside.call(());
}
}
});
});
use_drop(move || {
if let Some(eval) = eval_handle() {
let _ = eval.send("drop");
}
});
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FloatingResult {
pub x: f64,
pub y: f64,
pub is_ready: bool,
}
pub fn use_placement<E, T>(
element_ref: E,
trigger_ref: T,
options: FloatingOptions,
) -> ReadSignal<FloatingResult>
where
E: Into<ReadSignal<Option<Rc<MountedData>>>>,
T: Into<ReadSignal<Option<Rc<MountedData>>>>,
{
let element_ref = element_ref.into();
let trigger_ref = trigger_ref.into();
let floating = use_floating();
let mut result = use_signal(FloatingResult::default);
let context = match try_use_context::<ScrollableContext>() {
Some(ctx) => ctx,
None => {
tracing::warn!(
"use_placement hook used outside of ScrollableView. \
Ensure your component is wrapped in a ScrollableView or provide a ScrollableContext."
);
return result.into();
}
};
use_effect(move || {
let zip = (context.scroll_state)()
.zip((context.scrollable_ref)())
.zip(element_ref())
.zip(trigger_ref());
if let Some((((scroll_state, scrollable), element), trigger)) = zip {
let options = options.clone();
spawn(async move {
gloo_timers::future::TimeoutFuture::new(1).await;
let pos = floating
.placement_on_trigger(scroll_state, scrollable, element, trigger, options)
.await;
result.set(FloatingResult {
x: pos.0,
y: pos.1,
is_ready: true,
});
tracing::debug!(
"Floating placement updated: x={}, y={}, ready=true",
pos.0,
pos.1
);
});
} else {
if result.peek().is_ready {
result.set(FloatingResult::default());
tracing::debug!("Floating placement reset: ready=false");
}
}
});
result.into()
}
pub fn use_placement_on_point<E, T>(
element_ref: E,
trigger_point: T,
options: FloatingOptions,
) -> ReadSignal<FloatingResult>
where
E: Into<ReadSignal<Option<Rc<MountedData>>>>,
T: Into<ReadSignal<Option<ClientPoint>>>,
{
let element_ref = element_ref.into();
let trigger_point = trigger_point.into();
let floating = use_floating();
let mut result = use_signal(FloatingResult::default);
let context = match try_use_context::<ScrollableContext>() {
Some(ctx) => ctx,
None => {
tracing::warn!(
"use_placement hook used outside of ScrollableView. \
Ensure your component is wrapped in a ScrollableView or provide a ScrollableContext."
);
return result.into();
}
};
use_effect(move || {
let zip = (context.scroll_state)()
.zip((context.scrollable_ref)())
.zip(element_ref())
.zip(trigger_point());
if let Some((((scroll_state, scrollable), element), trigger)) = zip {
let options = options.clone();
spawn(async move {
gloo_timers::future::TimeoutFuture::new(1).await;
let pos = floating
.placement_on_point(scroll_state, scrollable, element, trigger, options)
.await;
result.set(FloatingResult {
x: pos.0,
y: pos.1,
is_ready: true,
});
tracing::debug!(
"Floating placement updated: x={}, y={}, ready=true",
pos.0,
pos.1
);
});
} else {
if result.peek().is_ready {
result.set(FloatingResult::default());
tracing::debug!("Floating placement reset: ready=false");
}
}
});
result.into()
}