use crate::core::{Direction, Directions, IntoElementMaybeSignal};
use crate::UseEventListenerOptions;
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder;
use leptos::prelude::*;
use leptos::reactive::wrappers::read::Signal;
use std::rc::Rc;
cfg_if! { if #[cfg(not(feature = "ssr"))] {
use crate::use_event_listener::use_event_listener_with_options;
use crate::{
sendwrap_fn, use_debounce_fn_with_arg, use_throttle_fn_with_arg_and_options, ThrottleOptions,
};
use leptos::ev;
use leptos::ev::scrollend;
use wasm_bindgen::JsCast;
const ARRIVED_STATE_THRESHOLD_PIXELS: f64 = 1.0;
}}
pub fn use_scroll<El, M>(
element: El,
) -> UseScrollReturn<
impl Fn(f64) + Clone + Send + Sync,
impl Fn(f64) + Clone + Send + Sync,
impl Fn() + Clone + Send + Sync,
>
where
El: IntoElementMaybeSignal<web_sys::Element, M>,
{
use_scroll_with_options(element, Default::default())
}
#[cfg_attr(feature = "ssr", allow(unused_variables))]
pub fn use_scroll_with_options<El, M>(
element: El,
options: UseScrollOptions,
) -> UseScrollReturn<
impl Fn(f64) + Clone + Send + Sync,
impl Fn(f64) + Clone + Send + Sync,
impl Fn() + Clone + Send + Sync,
>
where
El: IntoElementMaybeSignal<web_sys::Element, M>,
{
let (internal_x, set_internal_x) = signal(0.0);
let (internal_y, set_internal_y) = signal(0.0);
let (is_scrolling, set_is_scrolling) = signal(false);
let arrived_state = RwSignal::new(Directions {
left: true,
right: false,
top: true,
bottom: false,
});
let directions = RwSignal::new(Directions {
left: false,
right: false,
top: false,
bottom: false,
});
let set_x;
let set_y;
let measure;
#[cfg(feature = "ssr")]
{
set_x = |_| {};
set_y = |_| {};
measure = || {};
}
#[cfg(not(feature = "ssr"))]
{
let signal = element.into_element_maybe_signal();
let behavior = options.behavior;
let scroll_to = move |x: Option<f64>, y: Option<f64>| {
let element = signal.get_untracked();
if let Some(element) = element {
let scroll_options = web_sys::ScrollToOptions::new();
scroll_options.set_behavior(behavior.get_untracked().into());
if let Some(x) = x {
scroll_options.set_left(x);
}
if let Some(y) = y {
scroll_options.set_top(y);
}
element.scroll_to_with_scroll_to_options(&scroll_options);
}
};
set_x = sendwrap_fn!(move |x| scroll_to(Some(x), None));
set_y = sendwrap_fn!(move |y| scroll_to(None, Some(y)));
let on_scroll_end = {
let on_stop = Rc::clone(&options.on_stop);
move |e| {
if !is_scrolling.try_get_untracked().unwrap_or_default() {
return;
}
set_is_scrolling.set(false);
directions.update(|directions| {
directions.left = false;
directions.right = false;
directions.top = false;
directions.bottom = false;
on_stop.clone()(e);
});
}
};
let throttle = options.throttle;
let on_scroll_end_debounced =
use_debounce_fn_with_arg(on_scroll_end.clone(), throttle + options.idle);
let offset = options.offset;
let set_arrived_state = move |target: web_sys::Element| {
let style = window()
.get_computed_style(&target)
.expect("failed to get computed style");
if let Some(style) = style {
let display = style
.get_property_value("display")
.expect("failed to get display");
let flex_direction = style
.get_property_value("flex-direction")
.expect("failed to get flex-direction");
let scroll_left = target.scroll_left() as f64;
let scroll_left_abs = scroll_left.abs();
directions.update(|directions| {
directions.left = scroll_left < internal_x.get_untracked();
directions.right = scroll_left > internal_x.get_untracked();
});
let left = scroll_left_abs <= offset.left;
let right = scroll_left_abs + target.client_width() as f64
>= target.scroll_width() as f64 - offset.right - ARRIVED_STATE_THRESHOLD_PIXELS;
arrived_state.update(|arrived_state| {
if display == "flex" && flex_direction == "row-reverse" {
arrived_state.left = right;
arrived_state.right = left;
} else {
arrived_state.left = left;
arrived_state.right = right;
}
});
set_internal_x.set(scroll_left);
let mut scroll_top = target.scroll_top() as f64;
if target == document().unchecked_into::<web_sys::Element>() && scroll_top == 0.0 {
scroll_top = document().body().expect("failed to get body").scroll_top() as f64;
}
let scroll_top_abs = scroll_top.abs();
directions.update(|directions| {
directions.top = scroll_top < internal_y.get_untracked();
directions.bottom = scroll_top > internal_y.get_untracked();
});
let top = scroll_top_abs <= offset.top;
let bottom = scroll_top_abs + target.client_height() as f64
>= target.scroll_height() as f64
- offset.bottom
- ARRIVED_STATE_THRESHOLD_PIXELS;
arrived_state.update(|arrived_state| {
if display == "flex" && flex_direction == "column-reverse" {
arrived_state.top = bottom;
arrived_state.bottom = top;
} else {
arrived_state.top = top;
arrived_state.bottom = bottom;
}
});
set_internal_y.set(scroll_top);
}
};
let on_scroll_handler = {
let on_scroll = Rc::clone(&options.on_scroll);
move |e: web_sys::Event| {
let target: web_sys::Element = event_target(&e);
set_arrived_state(target);
set_is_scrolling.set(true);
on_scroll_end_debounced.clone()(e.clone());
on_scroll.clone()(e);
}
};
let target = Signal::derive_local(move || {
let element = signal.get();
element.map(|element| element.unchecked_into::<web_sys::EventTarget>())
});
if throttle >= 0.0 {
let throttled_scroll_handler = use_throttle_fn_with_arg_and_options(
on_scroll_handler.clone(),
throttle,
ThrottleOptions {
trailing: true,
leading: false,
},
);
let handler = move |e: web_sys::Event| {
throttled_scroll_handler.clone()(e);
};
let _ = use_event_listener_with_options::<
_,
Signal<Option<web_sys::EventTarget>, LocalStorage>,
_,
_,
>(target, ev::scroll, handler, options.event_listener_options);
} else {
let _ = use_event_listener_with_options::<
_,
Signal<Option<web_sys::EventTarget>, LocalStorage>,
_,
_,
>(
target,
ev::scroll,
on_scroll_handler,
options.event_listener_options,
);
}
let _ = use_event_listener_with_options::<
_,
Signal<Option<web_sys::EventTarget>, LocalStorage>,
_,
_,
>(
target,
scrollend,
on_scroll_end,
options.event_listener_options,
);
measure = sendwrap_fn!(move || {
if let Some(el) = signal.try_get_untracked().flatten() {
set_arrived_state(el);
}
});
}
UseScrollReturn {
x: internal_x.into(),
set_x,
y: internal_y.into(),
set_y,
is_scrolling: is_scrolling.into(),
arrived_state: arrived_state.into(),
directions: directions.into(),
measure,
}
}
#[derive(DefaultBuilder)]
#[cfg_attr(feature = "ssr", allow(dead_code))]
pub struct UseScrollOptions {
throttle: f64,
idle: f64,
offset: ScrollOffset,
on_scroll: Rc<dyn Fn(web_sys::Event)>,
on_stop: Rc<dyn Fn(web_sys::Event)>,
event_listener_options: UseEventListenerOptions,
#[builder(into)]
behavior: Signal<ScrollBehavior>,
}
impl Default for UseScrollOptions {
fn default() -> Self {
Self {
throttle: 0.0,
idle: 200.0,
offset: ScrollOffset::default(),
on_scroll: Rc::new(|_| {}),
on_stop: Rc::new(|_| {}),
event_listener_options: Default::default(),
behavior: Default::default(),
}
}
}
#[derive(Default, Copy, Clone)]
pub enum ScrollBehavior {
#[default]
Auto,
Smooth,
}
impl From<ScrollBehavior> for web_sys::ScrollBehavior {
fn from(val: ScrollBehavior) -> Self {
match val {
ScrollBehavior::Auto => web_sys::ScrollBehavior::Auto,
ScrollBehavior::Smooth => web_sys::ScrollBehavior::Smooth,
}
}
}
pub struct UseScrollReturn<SetXFn, SetYFn, MFn>
where
SetXFn: Fn(f64) + Clone + Send + Sync,
SetYFn: Fn(f64) + Clone + Send + Sync,
MFn: Fn() + Clone + Send + Sync,
{
pub x: Signal<f64>,
pub set_x: SetXFn,
pub y: Signal<f64>,
pub set_y: SetYFn,
pub is_scrolling: Signal<bool>,
pub arrived_state: Signal<Directions>,
pub directions: Signal<Directions>,
pub measure: MFn,
}
#[derive(Default, Copy, Clone, Debug)]
pub struct ScrollOffset {
pub left: f64,
pub top: f64,
pub right: f64,
pub bottom: f64,
}
impl ScrollOffset {
pub fn set_direction(mut self, direction: Direction, value: f64) -> Self {
match direction {
Direction::Top => self.top = value,
Direction::Bottom => self.bottom = value,
Direction::Left => self.left = value,
Direction::Right => self.right = value,
}
self
}
}