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}