leptos_use/use_intersection_observer.rs
1use crate::core::{IntoElementMaybeSignal, IntoElementsMaybeSignal};
2use crate::sendwrap_fn;
3use cfg_if::cfg_if;
4use default_struct_builder::DefaultBuilder;
5use leptos::prelude::*;
6use leptos::reactive::wrappers::read::Signal;
7use std::marker::PhantomData;
8
9cfg_if! { if #[cfg(not(feature = "ssr"))] {
10 use crate::{watch_with_options, WatchOptions};
11 // use std::cell::RefCell;
12 // use std::rc::Rc;
13 use std::sync::{Arc, Mutex};
14 use wasm_bindgen::prelude::*;
15}}
16
17/// Reactive [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver).
18///
19/// Detects that a target element's visibility inside the viewport.
20///
21/// ## Demo
22///
23/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_intersection_observer)
24///
25/// ## Usage
26///
27/// ```
28/// # use leptos::prelude::*;
29/// # use leptos::html::Div;
30/// # use leptos_use::use_intersection_observer;
31/// #
32/// # #[component]
33/// # fn Demo() -> impl IntoView {
34/// let el = NodeRef::<Div>::new();
35/// let (is_visible, set_visible) = signal(false);
36///
37/// use_intersection_observer(
38/// el,
39/// move |entries, _| {
40/// set_visible.set(entries[0].is_intersecting());
41/// },
42/// );
43///
44/// view! {
45/// <div node_ref=el>
46/// <h1>"Hello World"</h1>
47/// </div>
48/// }
49/// # }
50/// ```
51///
52/// ## SendWrapped Return
53///
54/// The returned closures `pause`, `resume` and `stop` are sendwrapped functions. They can
55/// only be called from the same thread that called `use_intersection_observer`.
56///
57/// ## Server-Side Rendering
58///
59/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
60///
61/// On the server this amounts to a no-op.
62///
63/// ## See also
64///
65/// * [`fn@crate::use_element_visibility`]
66pub fn use_intersection_observer<Els, M, F, RootM>(
67 target: Els,
68 callback: F,
69) -> UseIntersectionObserverReturn<
70 impl Fn() + Clone + Send + Sync,
71 impl Fn() + Clone + Send + Sync,
72 impl Fn() + Clone + Send + Sync,
73>
74where
75 Els: IntoElementsMaybeSignal<web_sys::Element, M>,
76 F: FnMut(Vec<web_sys::IntersectionObserverEntry>, web_sys::IntersectionObserver) + 'static,
77 web_sys::Element: IntoElementMaybeSignal<web_sys::Element, RootM>,
78{
79 use_intersection_observer_with_options::<Els, M, web_sys::Element, RootM, F>(
80 target,
81 callback,
82 UseIntersectionObserverOptions::default(),
83 )
84}
85
86/// Version of [`use_intersection_observer`] that takes a [`UseIntersectionObserverOptions`]. See [`use_intersection_observer`] for how to use.
87#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
88pub fn use_intersection_observer_with_options<Els, M, RootEl, RootM, F>(
89 target: Els,
90 mut callback: F,
91 options: UseIntersectionObserverOptions<RootEl, RootM>,
92) -> UseIntersectionObserverReturn<
93 impl Fn() + Clone + Send + Sync,
94 impl Fn() + Clone + Send + Sync,
95 impl Fn() + Clone + Send + Sync,
96>
97where
98 Els: IntoElementsMaybeSignal<web_sys::Element, M>,
99 RootEl: IntoElementMaybeSignal<web_sys::Element, RootM>,
100 F: FnMut(Vec<web_sys::IntersectionObserverEntry>, web_sys::IntersectionObserver) + 'static,
101{
102 let UseIntersectionObserverOptions {
103 immediate,
104 root,
105 root_margin,
106 thresholds,
107 ..
108 } = options;
109
110 let (is_active, set_active) = signal(immediate);
111
112 let pause;
113 let cleanup;
114 let stop;
115
116 #[cfg(feature = "ssr")]
117 {
118 pause = || {};
119 cleanup = || {};
120 stop = || {};
121 }
122
123 #[cfg(not(feature = "ssr"))]
124 {
125 use send_wrapper::SendWrapper;
126
127 let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::IntersectionObserver)>::new(
128 move |entries: js_sys::Array, observer| {
129 #[cfg(debug_assertions)]
130 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
131
132 callback(
133 entries
134 .to_vec()
135 .into_iter()
136 .map(|v| v.unchecked_into::<web_sys::IntersectionObserverEntry>())
137 .collect(),
138 observer,
139 );
140 },
141 )
142 .into_js_value();
143
144 let observer: Arc<Mutex<Option<SendWrapper<web_sys::IntersectionObserver>>>> =
145 Arc::new(Mutex::new(None));
146
147 cleanup = {
148 let observer = Arc::clone(&observer);
149
150 move || {
151 if let Some(o) = observer.lock().unwrap().take() {
152 o.disconnect();
153 }
154 }
155 };
156
157 let targets = target.into_elements_maybe_signal();
158 let root = root.map(|root| root.into_element_maybe_signal());
159
160 let stop_watch = {
161 let cleanup = cleanup.clone();
162
163 watch_with_options(
164 move || {
165 (
166 targets.get(),
167 root.as_ref().map(|root| root.get()),
168 is_active.get(),
169 )
170 },
171 move |values, _, _| {
172 let (targets, root, is_active) = values;
173
174 cleanup();
175
176 if !is_active {
177 return;
178 }
179
180 let options = web_sys::IntersectionObserverInit::new();
181 options.set_root_margin(&root_margin);
182 options.set_threshold(
183 &thresholds
184 .iter()
185 .copied()
186 .map(JsValue::from)
187 .collect::<js_sys::Array>(),
188 );
189
190 if let Some(Some(root)) = root {
191 let root = root.clone();
192 options.set_root(Some(&root));
193 }
194
195 let obs = web_sys::IntersectionObserver::new_with_options(
196 closure_js.clone().as_ref().unchecked_ref(),
197 &options,
198 )
199 .expect("failed to create IntersectionObserver");
200
201 for target in targets.iter().flatten() {
202 let target = target.clone();
203 obs.observe(&target);
204 }
205
206 *observer.lock().unwrap() = Some(SendWrapper::new(obs));
207 },
208 WatchOptions::default().immediate(immediate),
209 )
210 };
211
212 stop = {
213 let cleanup = cleanup.clone();
214
215 sendwrap_fn!(move || {
216 cleanup();
217 stop_watch();
218 })
219 };
220
221 on_cleanup(stop.clone());
222
223 pause = {
224 let cleanup = cleanup.clone();
225
226 sendwrap_fn!(move || {
227 cleanup();
228 set_active.set(false);
229 })
230 };
231 }
232
233 UseIntersectionObserverReturn {
234 is_active: is_active.into(),
235 pause,
236 resume: sendwrap_fn!(move || {
237 cleanup();
238 set_active.set(true);
239 }),
240 stop,
241 }
242}
243
244/// Options for [`use_intersection_observer_with_options`].
245#[derive(DefaultBuilder)]
246pub struct UseIntersectionObserverOptions<El, M>
247where
248 El: IntoElementMaybeSignal<web_sys::Element, M>,
249{
250 /// If `true`, the `IntersectionObserver` will be attached immediately. Otherwise it
251 /// will only be attached after the returned `resume` closure is called. That is
252 /// `use_intersections_observer` will be started "paused".
253 immediate: bool,
254
255 /// A `web_sys::Element` or `web_sys::Document` object which is an ancestor of the intended `target`,
256 /// whose bounding rectangle will be considered the viewport.
257 /// Any part of the target not visible in the visible area of the `root` is not considered visible.
258 /// Defaults to `None` (which means the root `document` will be used).
259 /// Please note that setting this to a `Some(document)` may not be supported by all browsers.
260 /// See [Browser Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#browser_compatibility)
261 root: Option<El>,
262
263 /// A string which specifies a set of offsets to add to the root's [bounding box](https://developer.mozilla.org/en-US/docs/Glossary/Bounding_box)
264 /// when calculating intersections, effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that for the CSS
265 /// [`margin`](https://developer.mozilla.org/en-US/docs/Web/CSS/margin) property; see
266 /// [The intersection root and root margin](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#the_intersection_root_and_root_margin)
267 /// for more information on how the margin works and the syntax. The default is `"0px"`.
268 #[builder(into)]
269 root_margin: String,
270
271 // TODO : validate that each number is between 0 and 1 ?
272 /// A `Vec` of numbers between 0.0 and 1.0, specifying a ratio of intersection area to total
273 /// bounding box area for the observed target. A value of 0.0 means that even a single
274 /// visible pixel counts as the target being visible. 1.0 means that the entire target
275 /// element is visible. See [Thresholds](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#thresholds)
276 /// for a more in-depth description of how thresholds are used.
277 /// The default is a single threshold of `[0.0]`.
278 thresholds: Vec<f64>,
279
280 #[builder(skip)]
281 _marker: PhantomData<M>,
282}
283
284impl<M> Default for UseIntersectionObserverOptions<web_sys::Element, M>
285where
286 web_sys::Element: IntoElementMaybeSignal<web_sys::Element, M>,
287{
288 fn default() -> Self {
289 Self {
290 immediate: true,
291 root: None,
292 root_margin: "0px".into(),
293 thresholds: vec![0.0],
294 _marker: PhantomData,
295 }
296 }
297}
298
299/// The return value of [`use_intersection_observer`].
300pub struct UseIntersectionObserverReturn<StopFn, PauseFn, ResumeFn>
301where
302 StopFn: Fn() + Clone + Send + Sync,
303 PauseFn: Fn() + Clone + Send + Sync,
304 ResumeFn: Fn() + Clone + Send + Sync,
305{
306 /// Pauses the `IntersectionObserver` observations. Will cause `is_active = false`.
307 pub pause: PauseFn,
308 /// Resumes the `IntersectionObserver` observations. Will cause `is_active = true`.
309 pub resume: ResumeFn,
310 /// Stops the `IntersectionObserver` observations altogether.
311 pub stop: StopFn,
312 /// A signal which is `true` when the `IntersectionObserver` is active, and `false` when paused or stopped.
313 pub is_active: Signal<bool>,
314}