diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index db5be54..60999d5 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -1,5 +1,10 @@ + + + + + @@ -10,5 +15,6 @@ + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index deec265..b785151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ repository = "https://github.com/Synphonyte/leptos-use" leptos = "0.3" web-sys = "0.3" wasm-bindgen = "0.2" +js-sys = "0.3" \ No newline at end of file diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 32e473e..6afe548 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -10,3 +10,7 @@ # Browser - [use_event_listener](browser/use_event_listener.md) + +# Utilities + +- [use_throttle_fn](utilities/use_throttle_fn.md) \ No newline at end of file diff --git a/docs/book/src/extract_doc_comment.py b/docs/book/src/extract_doc_comment.py index f7a0709..a2f831a 100644 --- a/docs/book/src/extract_doc_comment.py +++ b/docs/book/src/extract_doc_comment.py @@ -6,8 +6,10 @@ def main(): file_name = f"../../../../src/{name}.rs" with open(file_name) as f: in_code_block = False + doc_comment_started = False for line in f.readlines(): if line.startswith("///"): + doc_comment_started = True line = line.strip().replace("/// ", "").replace("///", "") if "```" in line: if not in_code_block: @@ -15,6 +17,8 @@ def main(): in_code_block = not in_code_block print(line) + elif doc_comment_started: + return if __name__ == '__main__': diff --git a/docs/book/src/utilities/use_throttle_fn.md b/docs/book/src/utilities/use_throttle_fn.md new file mode 100644 index 0000000..faf261f --- /dev/null +++ b/docs/book/src/utilities/use_throttle_fn.md @@ -0,0 +1,3 @@ +# use_throttle_fn + + diff --git a/src/lib.rs b/src/lib.rs index 8b4d75b..98ed266 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ pub mod core; pub mod use_event_listener; pub mod use_scroll; +pub mod use_throttle_fn; +pub mod utils; pub use use_event_listener::use_event_listener; -pub use use_scroll::use_scroll; +pub use use_scroll::*; +pub use use_throttle_fn::*; diff --git a/src/use_event_listener.rs b/src/use_event_listener.rs index 77d1237..696e04c 100644 --- a/src/use_event_listener.rs +++ b/src/use_event_listener.rs @@ -1,9 +1,7 @@ use crate::core::EventTargetMaybeSignal; use leptos::ev::EventDescriptor; -use leptos::html::ElementDescriptor; use leptos::*; use std::cell::RefCell; -use std::ops::Deref; use std::rc::Rc; use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; diff --git a/src/use_scroll.rs b/src/use_scroll.rs index 09470c3..0fbc2ba 100644 --- a/src/use_scroll.rs +++ b/src/use_scroll.rs @@ -1,6 +1,20 @@ +use crate::core::EventTargetMaybeSignal; use crate::use_event_listener; use leptos::*; +// pub fn use_scroll( +// cx: Scope, +// element: El, +// options: UseScrollOptions, +// ) -> UseScrollReturn +// where +// (Scope, El): Into>, +// T: Into + Clone + 'static, +// { +// } + +/// Options for [`use_scroll`]. +#[derive(Default)] pub struct UseScrollOptions { /// Throttle time in milliseconds for the scroll events. Defaults to 0 (disabled). pub throttle: u32, @@ -8,18 +22,46 @@ pub struct UseScrollOptions { /// After scrolling ends we wait idle + throttle milliseconds before we consider scrolling to have stopped. /// Defaults to 200. pub idle: u32, + + /// Threshold in pixels when we consider a side to have arrived (`UseScrollReturn::arrived_state`). + pub offset: ScrollOffset, + + /// Callback when scrolling is happening. + pub on_scroll: Option>, + + /// Callback when scrolling stops (after `idle` + `throttle` milliseconds have passed). + pub on_stop: Option>, + + /// Options passed to the `addEventListener("scroll", ...)` call + pub event_listener_options: Option, + + /// When changing the `x` or `y` signals this specifies the scroll behaviour. + /// Can be `Auto` (= not smooth) or `Smooth`. Defaults to `Auto`. + pub behavior: ScrollBehavior, } -pub struct UseScrollReturn { - pub x: ReadSignal, - pub setX: WriteSignal, - pub y: ReadSignal, - pub setY: WriteSignal, - pub isScrolling: ReadSignal, - pub arrivedState: ReadSignal, - pub directions: ReadSignal, +#[derive(Default)] +pub enum ScrollBehavior { + #[default] + Auto, + Smooth, } +pub struct UseScrollReturn +where + Fx: FnMut(f64), + Fy: FnMut(f64), +{ + pub x: Signal, + pub set_x: Fx, + pub y: Signal, + pub set_y: Fy, + pub is_scrolling: Signal, + pub arrived_state: Signal, + pub directions: Signal, +} + +#[derive(Copy, Clone)] pub struct Directions { pub left: bool, pub right: bool, @@ -27,4 +69,10 @@ pub struct Directions { pub bottom: bool, } -pub fn use_scroll() {} +#[derive(Default)] +pub struct ScrollOffset { + pub left: f64, + pub top: f64, + pub right: f64, + pub bottom: f64, +} diff --git a/src/use_throttle_fn.rs b/src/use_throttle_fn.rs new file mode 100644 index 0000000..4125464 --- /dev/null +++ b/src/use_throttle_fn.rs @@ -0,0 +1,116 @@ +use crate::utils::{create_filter_wrapper, create_filter_wrapper_with_arg, throttle_filter}; +use leptos::MaybeSignal; +use std::cell::RefCell; +use std::rc::Rc; + +pub use crate::utils::ThrottleOptions; + +/// Throttle execution of a function. +/// Especially useful for rate limiting execution of handlers on events like resize and scroll. +/// +/// > Throttle is a spring that throws balls: after a ball flies out it needs some time to shrink back, so it cannot throw any more balls unless it's ready. +/// +/// ## Demo +/// +/// TODO +/// +/// ## Usage +/// +/// ``` +/// use leptos::*; +/// use leptos_use::use_throttle_fn; +/// +/// #[component] +/// fn Demo(cx: Scope) -> impl IntoView { +/// let mut throttled_fn = use_throttle_fn( +/// || { +/// // do something, it will be called at most 1 time per second +/// }, +/// 1000.0, +/// ); +/// view! { cx, +/// +/// } +/// } +/// ``` +/// +/// You can provide options when you use [`use_throttle_fn_with_options`]. +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::{ThrottleOptions, use_throttle_fn_with_options}; +/// +/// # #[component] +/// # fn Demo(cx: Scope) -> impl IntoView { +/// let throttled_fn = use_throttle_fn_with_options( +/// || { +/// // do something, it will be called at most 1 time per second +/// }, +/// 1000.0, +/// ThrottleOptions { +/// leading: true, +/// trailing: true, +/// } +/// ); +/// # view! { cx, } +/// # } +/// ``` +/// +/// If your function that you want to throttle takes an argument there are also the versions +/// [`use_throttle_fn_with_args`] and [`use_throttle_fn_with_args_and_options`]. +/// +/// ## Recommended Reading +/// +/// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) +pub fn use_throttle_fn( + func: F, + ms: impl Into>, +) -> impl FnMut() -> Rc>> +where + F: FnMut() -> R + Clone + 'static, + R: 'static, +{ + use_throttle_fn_with_options(func, ms, Default::default()) +} + +/// Version of [`use_throttle_fn`] with throttle options. See the docs for [`use_throttle_fn`] for how to use. +pub fn use_throttle_fn_with_options( + func: F, + ms: impl Into>, + options: ThrottleOptions, +) -> impl FnMut() -> Rc>> +where + F: FnMut() -> R + Clone + 'static, + R: 'static, +{ + create_filter_wrapper(throttle_filter(ms, options), func) +} + +/// Version of [`use_throttle_fn`] with an argument for the throttled function. See the docs for [`use_throttle_fn`] for how to use. +pub fn use_throttle_fn_with_arg( + func: F, + ms: impl Into>, +) -> impl FnMut(Arg) -> Rc>> +where + F: FnMut(Arg) -> R + Clone + 'static, + Arg: 'static, + R: 'static, +{ + use_throttle_fn_with_arg_and_options(func, ms, Default::default()) +} + +/// Version of [`use_throttle_fn_with_arg`] with throttle options. See the docs for [`use_throttle_fn`] for how to use. +pub fn use_throttle_fn_with_arg_and_options( + func: F, + ms: impl Into>, + options: ThrottleOptions, +) -> impl FnMut(Arg) -> Rc>> +where + F: FnMut(Arg) -> R + Clone + 'static, + Arg: 'static, + R: 'static, +{ + create_filter_wrapper_with_arg(throttle_filter(ms, options), func) +} diff --git a/src/utils/filters.rs b/src/utils/filters.rs new file mode 100644 index 0000000..bc5fa38 --- /dev/null +++ b/src/utils/filters.rs @@ -0,0 +1,136 @@ +use js_sys::Date; +use leptos::leptos_dom::helpers::TimeoutHandle; +use leptos::{set_timeout_with_handle, MaybeSignal, Scope, SignalGetUntracked}; +use std::cell::{Cell, RefCell}; +use std::cmp::max; +use std::rc::Rc; +use std::time::Duration; + +pub fn create_filter_wrapper( + mut filter: Filter, + func: F, +) -> impl FnMut() -> Rc>> +where + F: FnMut() -> R + Clone + 'static, + R: 'static, + Filter: FnMut(Box R>) -> Rc>>, +{ + move || { + let wrapped_func = Box::new(func.clone()); + filter(wrapped_func) + } +} + +pub fn create_filter_wrapper_with_arg( + mut filter: Filter, + func: F, +) -> impl FnMut(Arg) -> Rc>> +where + F: FnMut(Arg) -> R + Clone + 'static, + R: 'static, + Arg: 'static, + Filter: FnMut(Box R>) -> Rc>>, +{ + move |arg: Arg| { + let mut func = func.clone(); + let wrapped_func = Box::new(move || func(arg)); + filter(wrapped_func) + } +} + +#[derive(Copy, Clone)] +pub struct ThrottleOptions { + pub trailing: bool, + pub leading: bool, +} + +impl Default for ThrottleOptions { + fn default() -> Self { + Self { + trailing: true, + leading: true, + } + } +} + +pub fn throttle_filter( + ms: impl Into>, + options: ThrottleOptions, +) -> impl FnMut(Box R>) -> Rc>> +where + R: 'static, +{ + let last_exec = Rc::new(Cell::new(0_f64)); + let timer = Rc::new(Cell::new(None::)); + let is_leading = Rc::new(Cell::new(true)); + let last_value: Rc>> = Rc::new(RefCell::new(None)); + + let t = Rc::clone(&timer); + let clear = move || { + if let Some(handle) = t.get() { + handle.clear(); + t.set(None); + } + }; + + let ms = ms.into(); + + move |mut _invoke: Box R>| { + let duration = ms.get_untracked(); + let elapsed = Date::now() - last_exec.get(); + + let last_val = Rc::clone(&last_value); + let mut invoke = move || { + let return_value = _invoke(); + + let mut val_mut = last_val.borrow_mut(); + *val_mut = Some(return_value); + }; + + let clear = clear.clone(); + clear(); + + if duration <= 0.0 { + last_exec.set(Date::now()); + invoke(); + return Rc::clone(&last_value); + } + + if elapsed > duration && (options.leading || !is_leading.get()) { + last_exec.set(Date::now()); + invoke(); + } else if options.trailing { + let last_exec = Rc::clone(&last_exec); + let is_leading = Rc::clone(&is_leading); + timer.set( + set_timeout_with_handle( + move || { + last_exec.set(Date::now()); + is_leading.set(true); + invoke(); + clear(); + }, + Duration::from_millis(max(0, (duration - elapsed) as u64)), + ) + .ok(), + ); + } + + if !options.leading && timer.get().is_none() { + let is_leading = Rc::clone(&is_leading); + timer.set( + set_timeout_with_handle( + move || { + is_leading.set(true); + }, + Duration::from_millis(duration as u64), + ) + .ok(), + ); + } + + is_leading.set(false); + + Rc::clone(&last_value) + } +} diff --git a/src/utils/is.rs b/src/utils/is.rs new file mode 100644 index 0000000..55cc3c5 --- /dev/null +++ b/src/utils/is.rs @@ -0,0 +1,3 @@ +pub fn noop() -> Box { + Box::new(|| {}) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..77594b8 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,5 @@ +mod filters; +mod is; + +pub use filters::*; +pub use is::*;