Skip to main content

leptos_use_intersection_observer/
use_intersection_observer.rs

1use crate::{
2    IntoElementMaybeSignal, IntoElementsMaybeSignal, UseIntersectionObserverOptions,
3    UseIntersectionObserverReturn, sendwrap_fn,
4};
5use cfg_if::cfg_if;
6use leptos::prelude::*;
7
8cfg_if! { if #[cfg(not(feature = "ssr"))] {
9    use crate::{watch_with_options, WatchOptions};
10    // use std::cell::RefCell;
11    // use std::rc::Rc;
12    use std::sync::{Arc, Mutex};
13    use wasm_bindgen::prelude::*;
14}}
15
16/// Reactive [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver).
17///
18/// Detects that a target element's visibility inside the viewport.
19///
20/// ## Demo
21///
22/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_intersection_observer)
23///
24/// ## Usage
25///
26/// ```
27/// # use leptos::prelude::*;
28/// # use leptos::html::Div;
29/// # use leptos_use_intersection_observer::use_intersection_observer;
30/// #
31/// # #[component]
32/// # fn Demo() -> impl IntoView {
33/// let el = NodeRef::<Div>::new();
34/// let (is_visible, set_visible) = signal(false);
35///
36/// use_intersection_observer(
37///     el,
38///     move |entries, _| {
39///         set_visible.set(entries[0].is_intersecting());
40///     },
41/// );
42///
43/// view! {
44///     <div node_ref=el>
45///         <h1>"Hello World"</h1>
46///     </div>
47/// }
48/// # }
49/// ```
50///
51/// ## SendWrapped Return
52///
53/// The returned closures `pause`, `resume` and `stop` are sendwrapped functions. They can
54/// only be called from the same thread that called `use_intersection_observer`.
55///
56/// ## Server-Side Rendering
57///
58/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
59///
60/// On the server this amounts to a no-op.
61///
62/// ## See also
63///
64/// * [`fn@crate::use_element_visibility`]
65pub fn use_intersection_observer<Els, M, F, RootM>(
66    target: Els,
67    callback: F,
68) -> UseIntersectionObserverReturn<
69    impl Fn() + Clone + Send + Sync,
70    impl Fn() + Clone + Send + Sync,
71    impl Fn() + Clone + Send + Sync,
72>
73where
74    Els: IntoElementsMaybeSignal<web_sys::Element, M>,
75    F: FnMut(Vec<web_sys::IntersectionObserverEntry>, web_sys::IntersectionObserver) + 'static,
76    web_sys::Element: IntoElementMaybeSignal<web_sys::Element, RootM>,
77{
78    use_intersection_observer_with_options::<Els, M, web_sys::Element, RootM, F>(
79        target,
80        callback,
81        UseIntersectionObserverOptions::default(),
82    )
83}
84
85/// Version of [`use_intersection_observer`] that takes a [`UseIntersectionObserverOptions`]. See [`use_intersection_observer`] for how to use.
86#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
87pub fn use_intersection_observer_with_options<Els, M, RootEl, RootM, F>(
88    target: Els,
89    mut callback: F,
90    options: UseIntersectionObserverOptions<RootEl, RootM>,
91) -> UseIntersectionObserverReturn<
92    impl Fn() + Clone + Send + Sync,
93    impl Fn() + Clone + Send + Sync,
94    impl Fn() + Clone + Send + Sync,
95>
96where
97    Els: IntoElementsMaybeSignal<web_sys::Element, M>,
98    RootEl: IntoElementMaybeSignal<web_sys::Element, RootM>,
99    F: FnMut(Vec<web_sys::IntersectionObserverEntry>, web_sys::IntersectionObserver) + 'static,
100{
101    let UseIntersectionObserverOptions {
102        immediate,
103        root,
104        root_margin,
105        thresholds,
106        ..
107    } = options;
108
109    let (is_active, set_active) = signal(immediate);
110
111    let pause;
112    let cleanup;
113    let stop;
114
115    #[cfg(feature = "ssr")]
116    {
117        pause = || {};
118        cleanup = || {};
119        stop = || {};
120    }
121
122    #[cfg(not(feature = "ssr"))]
123    {
124        use send_wrapper::SendWrapper;
125
126        let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::IntersectionObserver)>::new(
127            move |entries: js_sys::Array, observer| {
128                #[cfg(debug_assertions)]
129                let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
130
131                callback(
132                    entries
133                        .to_vec()
134                        .into_iter()
135                        .map(|v| v.unchecked_into::<web_sys::IntersectionObserverEntry>())
136                        .collect(),
137                    observer,
138                );
139            },
140        )
141        .into_js_value();
142
143        let observer: Arc<Mutex<Option<SendWrapper<web_sys::IntersectionObserver>>>> =
144            Arc::new(Mutex::new(None));
145
146        cleanup = {
147            let observer = Arc::clone(&observer);
148
149            move || {
150                if let Some(o) = observer.lock().unwrap().take() {
151                    o.disconnect();
152                }
153            }
154        };
155
156        let targets = target.into_elements_maybe_signal();
157        let root = root.map(|root| root.into_element_maybe_signal());
158
159        let stop_watch = {
160            let cleanup = cleanup.clone();
161
162            watch_with_options(
163                move || {
164                    (
165                        targets.get(),
166                        root.as_ref().map(|root| root.get()),
167                        is_active.get(),
168                    )
169                },
170                move |values, _, _| {
171                    let (targets, root, is_active) = values;
172
173                    cleanup();
174
175                    if !is_active {
176                        return;
177                    }
178
179                    let options = web_sys::IntersectionObserverInit::new();
180                    options.set_root_margin(&root_margin);
181                    options.set_threshold(
182                        &thresholds
183                            .iter()
184                            .copied()
185                            .map(JsValue::from)
186                            .collect::<js_sys::Array>(),
187                    );
188
189                    if let Some(Some(root)) = root {
190                        let root = root.clone();
191                        options.set_root(Some(&root));
192                    }
193
194                    let obs = web_sys::IntersectionObserver::new_with_options(
195                        closure_js.clone().as_ref().unchecked_ref(),
196                        &options,
197                    )
198                    .expect("failed to create IntersectionObserver");
199
200                    for target in targets.iter().flatten() {
201                        let target = target.clone();
202                        obs.observe(&target);
203                    }
204
205                    *observer.lock().unwrap() = Some(SendWrapper::new(obs));
206                },
207                WatchOptions::default().immediate(immediate),
208            )
209        };
210
211        stop = {
212            let cleanup = cleanup.clone();
213
214            sendwrap_fn!(move || {
215                cleanup();
216                stop_watch();
217            })
218        };
219
220        on_cleanup(stop.clone());
221
222        pause = {
223            let cleanup = cleanup.clone();
224
225            sendwrap_fn!(move || {
226                cleanup();
227                set_active.set(false);
228            })
229        };
230    }
231
232    UseIntersectionObserverReturn {
233        is_active: is_active.into(),
234        pause,
235        resume: sendwrap_fn!(move || {
236            cleanup();
237            set_active.set(true);
238        }),
239        stop,
240    }
241}