use or_poisoned::OrPoisoned;
#[cfg(debug_assertions)]
use reactive_graph::diagnostics::SpecialNonReactiveZone;
use reactive_graph::owner::Owner;
use send_wrapper::SendWrapper;
use std::time::Duration;
use tachys::html::event::EventDescriptor;
#[cfg(feature = "tracing")]
use tracing::instrument;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
thread_local! {
pub(crate) static WINDOW: web_sys::Window = web_sys::window().unwrap_throw();
pub(crate) static DOCUMENT: web_sys::Document = web_sys::window().unwrap_throw().document().unwrap_throw();
}
pub fn window() -> web_sys::Window {
WINDOW.with(Clone::clone)
}
pub fn document() -> web_sys::Document {
DOCUMENT.with(Clone::clone)
}
pub fn set_property(
el: &web_sys::Element,
prop_name: &str,
value: &Option<JsValue>,
) {
let key = JsValue::from_str(prop_name);
match value {
Some(value) => _ = js_sys::Reflect::set(el, &key, value),
None => _ = js_sys::Reflect::delete_property(el, &key),
};
}
#[doc(hidden)]
pub fn get_property(
el: &web_sys::Element,
prop_name: &str,
) -> Result<JsValue, JsValue> {
let key = JsValue::from_str(prop_name);
js_sys::Reflect::get(el, &key)
}
pub fn location() -> web_sys::Location {
window().location()
}
pub fn location_hash() -> Option<String> {
if is_server() {
None
} else {
location()
.hash()
.ok()
.map(|hash| match hash.chars().next() {
Some('#') => hash[1..].to_string(),
_ => hash,
})
}
}
pub fn location_pathname() -> Option<String> {
location().pathname().ok()
}
pub fn event_target<T>(event: &web_sys::Event) -> T
where
T: JsCast,
{
event.target().unwrap_throw().unchecked_into::<T>()
}
pub fn event_target_value<T>(event: &T) -> String
where
T: JsCast,
{
event
.unchecked_ref::<web_sys::Event>()
.target()
.unwrap_throw()
.unchecked_into::<web_sys::HtmlInputElement>()
.value()
}
pub fn event_target_checked(ev: &web_sys::Event) -> bool {
ev.target()
.unwrap()
.unchecked_into::<web_sys::HtmlInputElement>()
.checked()
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct AnimationFrameRequestHandle(i32);
impl AnimationFrameRequestHandle {
pub fn cancel(&self) {
_ = window().cancel_animation_frame(self.0);
}
}
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
_ = request_animation_frame_with_handle(cb);
}
fn closure_once(cb: impl FnOnce() + 'static) -> JsValue {
let mut wrapped_cb: Option<Box<dyn FnOnce()>> = Some(Box::new(cb));
let closure = Closure::new(move || {
if let Some(cb) = wrapped_cb.take() {
cb()
}
});
closure.into_js_value()
}
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame_with_handle(
cb: impl FnOnce() + 'static,
) -> Result<AnimationFrameRequestHandle, JsValue> {
#[cfg(feature = "tracing")]
let span = ::tracing::Span::current();
#[cfg(feature = "tracing")]
let cb = move || {
let _guard = span.enter();
cb();
};
#[inline(never)]
fn raf(cb: JsValue) -> Result<AnimationFrameRequestHandle, JsValue> {
window()
.request_animation_frame(cb.as_ref().unchecked_ref())
.map(AnimationFrameRequestHandle)
}
raf(closure_once(cb))
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IdleCallbackHandle(u32);
impl IdleCallbackHandle {
pub fn cancel(&self) {
window().cancel_idle_callback(self.0);
}
}
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback(cb: impl Fn() + 'static) {
_ = request_idle_callback_with_handle(cb);
}
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback_with_handle(
cb: impl Fn() + 'static,
) -> Result<IdleCallbackHandle, JsValue> {
#[cfg(feature = "tracing")]
let span = ::tracing::Span::current();
#[cfg(feature = "tracing")]
let cb = move || {
let _guard = span.enter();
cb();
};
#[inline(never)]
fn ric(cb: Box<dyn Fn()>) -> Result<IdleCallbackHandle, JsValue> {
let cb = Closure::wrap(cb).into_js_value();
window()
.request_idle_callback(cb.as_ref().unchecked_ref())
.map(IdleCallbackHandle)
}
ric(Box::new(cb))
}
pub fn queue_microtask(task: impl FnOnce() + 'static) {
use js_sys::{Function, Reflect};
let task = Closure::once_into_js(task);
let window = web_sys::window().expect("window not available");
let queue_microtask =
Reflect::get(&window, &JsValue::from_str("queueMicrotask"))
.expect("queueMicrotask not available");
let queue_microtask = queue_microtask.unchecked_into::<Function>();
_ = queue_microtask.call1(&JsValue::UNDEFINED, &task);
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TimeoutHandle(i32);
impl TimeoutHandle {
pub fn clear(&self) {
window().clear_timeout_with_handle(self.0);
}
}
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
_ = set_timeout_with_handle(cb, duration);
}
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[inline(always)]
pub fn set_timeout_with_handle(
cb: impl FnOnce() + 'static,
duration: Duration,
) -> Result<TimeoutHandle, JsValue> {
#[cfg(debug_assertions)]
let cb = || {
let _z = SpecialNonReactiveZone::enter();
cb();
};
#[cfg(feature = "tracing")]
let span = ::tracing::Span::current();
#[cfg(feature = "tracing")]
let cb = move || {
let _guard = span.enter();
cb();
};
#[inline(never)]
fn st(cb: JsValue, duration: Duration) -> Result<TimeoutHandle, JsValue> {
window()
.set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)
.map(TimeoutHandle)
}
st(closure_once(cb), duration)
}
pub fn debounce<T: 'static>(
delay: Duration,
mut cb: impl FnMut(T) + 'static,
) -> impl FnMut(T) {
use std::sync::{Arc, RwLock};
#[cfg(debug_assertions)]
#[allow(unused_mut)]
let mut cb = move |value| {
let _z = SpecialNonReactiveZone::enter();
cb(value);
};
#[cfg(feature = "tracing")]
let span = ::tracing::Span::current();
#[cfg(feature = "tracing")]
#[allow(unused_mut)]
let mut cb = move |value| {
let _guard = span.enter();
cb(value);
};
let cb = Arc::new(RwLock::new(cb));
let timer = Arc::new(RwLock::new(None::<TimeoutHandle>));
Owner::on_cleanup({
let timer = Arc::clone(&timer);
move || {
if let Some(timer) = timer.write().or_poisoned().take() {
timer.clear();
}
}
});
move |arg| {
if let Some(timer) = timer.write().unwrap().take() {
timer.clear();
}
let handle = set_timeout_with_handle(
{
let cb = Arc::clone(&cb);
move || {
cb.write().unwrap()(arg);
}
},
delay,
);
if let Ok(handle) = handle {
*timer.write().or_poisoned() = Some(handle);
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IntervalHandle(i32);
impl IntervalHandle {
pub fn clear(&self) {
window().clear_interval_with_handle(self.0);
}
}
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
_ = set_interval_with_handle(cb, duration);
}
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[inline(always)]
pub fn set_interval_with_handle(
cb: impl Fn() + 'static,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {
#[cfg(debug_assertions)]
let cb = move || {
let _z = SpecialNonReactiveZone::enter();
cb();
};
#[cfg(feature = "tracing")]
let span = ::tracing::Span::current();
#[cfg(feature = "tracing")]
let cb = move || {
let _guard = span.enter();
cb();
};
#[inline(never)]
fn si(
cb: Box<dyn Fn()>,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {
let cb = Closure::wrap(cb).into_js_value();
window()
.set_interval_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)
.map(IntervalHandle)
}
si(Box::new(cb), duration)
}
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(event_name = %event_name))
)]
#[inline(always)]
pub fn window_event_listener_untyped(
event_name: &str,
cb: impl Fn(web_sys::Event) + 'static,
) -> WindowListenerHandle {
#[cfg(debug_assertions)]
let cb = move |e| {
let _z = SpecialNonReactiveZone::enter();
cb(e);
};
#[cfg(feature = "tracing")]
let span = ::tracing::Span::current();
#[cfg(feature = "tracing")]
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
if !is_server() {
#[inline(never)]
fn wel(
cb: Box<dyn FnMut(web_sys::Event)>,
event_name: &str,
) -> WindowListenerHandle {
let cb = Closure::wrap(cb).into_js_value();
_ = window().add_event_listener_with_callback(
event_name,
cb.unchecked_ref(),
);
let event_name = event_name.to_string();
let cb = SendWrapper::new(cb);
WindowListenerHandle(Box::new(move || {
_ = window().remove_event_listener_with_callback(
&event_name,
cb.unchecked_ref(),
);
}))
}
wel(Box::new(cb), event_name)
} else {
WindowListenerHandle(Box::new(|| ()))
}
}
pub fn window_event_listener<E: EventDescriptor + 'static>(
event: E,
cb: impl Fn(E::EventType) + 'static,
) -> WindowListenerHandle
where
E::EventType: JsCast,
{
window_event_listener_untyped(&event.name(), move |e| {
cb(e.unchecked_into::<E::EventType>())
})
}
pub struct WindowListenerHandle(Box<dyn FnOnce() + Send + Sync>);
impl core::fmt::Debug for WindowListenerHandle {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("WindowListenerHandle").finish()
}
}
impl WindowListenerHandle {
pub fn remove(self) {
(self.0)()
}
}
fn is_server() -> bool {
#[cfg(feature = "hydration")]
{
Owner::current_shared_context()
.map(|sc| !sc.is_browser())
.unwrap_or(false)
}
#[cfg(not(feature = "hydration"))]
{
false
}
}