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