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::*;