leptos_use/
use_infinite_scroll.rs1use crate::core::{Direction, Directions, IntoElementMaybeSignal};
2use crate::{
3 ScrollOffset, UseEventListenerOptions, UseScrollOptions, UseScrollReturn,
4 use_element_visibility, use_scroll_with_options,
5};
6use default_struct_builder::DefaultBuilder;
7use futures_util::join;
8use gloo_timers::future::sleep;
9use leptos::prelude::*;
10use leptos::reactive::wrappers::read::Signal;
11use send_wrapper::SendWrapper;
12use std::future::Future;
13use std::sync::Arc;
14use std::time::Duration;
15use wasm_bindgen::JsCast;
16
17pub fn use_infinite_scroll<El, M, LFn, LFut>(el: El, on_load_more: LFn) -> Signal<bool>
55where
56 El: IntoElementMaybeSignal<web_sys::Element, M> + 'static,
57 LFn: Fn(ScrollState) -> LFut + Send + Sync + 'static,
58 LFut: Future<Output = ()>,
59{
60 use_infinite_scroll_with_options(el, on_load_more, UseInfiniteScrollOptions::default())
61}
62
63pub fn use_infinite_scroll_with_options<El, M, LFn, LFut>(
65 el: El,
66 on_load_more: LFn,
67 options: UseInfiniteScrollOptions,
68) -> Signal<bool>
69where
70 El: IntoElementMaybeSignal<web_sys::Element, M> + 'static,
71 LFn: Fn(ScrollState) -> LFut + Send + Sync + 'static,
72 LFut: Future<Output = ()>,
73{
74 let UseInfiniteScrollOptions {
75 distance,
76 direction,
77 interval,
78 on_scroll,
79 event_listener_options,
80 } = options;
81
82 let on_load_more = StoredValue::new(on_load_more);
83
84 let el = el.into_element_maybe_signal();
85
86 let UseScrollReturn {
87 x,
88 y,
89 is_scrolling,
90 arrived_state,
91 directions,
92 measure,
93 ..
94 } = use_scroll_with_options(
95 el,
96 UseScrollOptions::default()
97 .on_scroll(move |evt| on_scroll(evt))
98 .event_listener_options(event_listener_options)
99 .offset(ScrollOffset::default().set_direction(direction, distance)),
100 );
101
102 let state = ScrollState {
103 x,
104 y,
105 is_scrolling,
106 arrived_state,
107 directions,
108 };
109
110 let (is_loading, set_loading) = signal(false);
111
112 let observed_element = Signal::derive_local(move || {
113 let el = el.get();
114
115 el.map(|el| {
116 if el.is_instance_of::<web_sys::Window>() || el.is_instance_of::<web_sys::Document>() {
117 SendWrapper::new(
118 document()
119 .document_element()
120 .expect("document element not found"),
121 )
122 } else {
123 el
124 }
125 })
126 });
127
128 let is_element_visible = use_element_visibility(observed_element);
129
130 let check_and_load = StoredValue::new(None::<Arc<dyn Fn() + Send + Sync>>);
131
132 check_and_load.set_value(Some(Arc::new({
133 let measure = measure.clone();
134
135 move || {
136 let observed_element = observed_element.get_untracked();
137
138 if !is_element_visible.get_untracked() {
139 return;
140 }
141
142 if let Some(observed_element) = observed_element {
143 let scroll_height = observed_element.scroll_height();
144 let client_height = observed_element.client_height();
145 let scroll_width = observed_element.scroll_width();
146 let client_width = observed_element.client_width();
147
148 let is_narrower = if direction == Direction::Bottom || direction == Direction::Top {
149 scroll_height <= client_height
150 } else {
151 scroll_width <= client_width
152 };
153
154 if (state.arrived_state.get_untracked().get_direction(direction) || is_narrower)
155 && !is_loading.get_untracked()
156 {
157 set_loading.set(true);
158
159 let measure = measure.clone();
160 leptos::task::spawn_local(async move {
161 #[cfg(debug_assertions)]
162 let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
163
164 join!(
165 on_load_more.with_value(|f| f(state)),
166 sleep(Duration::from_millis(interval as u64))
167 );
168
169 #[cfg(debug_assertions)]
170 drop(zone);
171
172 set_loading.try_set(false);
173 sleep(Duration::ZERO).await;
174 measure();
175 if let Some(check_and_load) = check_and_load.try_get_value().flatten() {
176 check_and_load();
177 }
178 });
179 }
180 }
181 }
182 })));
183
184 Effect::watch(
185 move || is_element_visible.get(),
186 move |visible, prev_visible, _| {
187 if *visible && !prev_visible.copied().unwrap_or_default() {
188 measure();
189 }
190 },
191 true,
192 );
193
194 Effect::watch(
195 move || state.arrived_state.get().get_direction(direction),
196 move |arrived, prev_arrived, _| {
197 if let Some(prev_arrived) = prev_arrived
198 && prev_arrived == arrived
199 {
200 return;
201 }
202
203 check_and_load
204 .get_value()
205 .expect("check_and_load is set above")()
206 },
207 true,
208 );
209
210 is_loading.into()
211}
212
213#[derive(DefaultBuilder)]
215pub struct UseInfiniteScrollOptions {
216 on_scroll: Arc<dyn Fn(web_sys::Event) + Send + Sync>,
218
219 event_listener_options: UseEventListenerOptions,
221
222 distance: f64,
224
225 direction: Direction,
227
228 interval: f64,
230}
231
232impl Default for UseInfiniteScrollOptions {
233 fn default() -> Self {
234 Self {
235 on_scroll: Arc::new(|_| {}),
236 event_listener_options: Default::default(),
237 distance: 0.0,
238 direction: Direction::Bottom,
239 interval: 100.0,
240 }
241 }
242}
243
244#[derive(Copy, Clone)]
246pub struct ScrollState {
247 pub x: Signal<f64>,
249
250 pub y: Signal<f64>,
252
253 pub is_scrolling: Signal<bool>,
255
256 pub arrived_state: Signal<Directions>,
259
260 pub directions: Signal<Directions>,
262}