leptos_use/
on_click_outside.rs1use crate::core::{ElementsMaybeSignal, IntoElementMaybeSignal, IntoElementsMaybeSignal};
2use cfg_if::cfg_if;
3use default_struct_builder::DefaultBuilder;
4
5cfg_if! { if #[cfg(not(feature = "ssr"))] {
6 use leptos::prelude::*;
7 use crate::utils::IS_IOS;
8 use crate::{use_event_listener, use_event_listener_with_options, UseEventListenerOptions, sendwrap_fn};
9 use leptos::ev::{blur, click, pointerdown};
10 use std::cell::Cell;
11 use std::rc::Rc;
12 use std::sync::RwLock;
13 use std::time::Duration;
14 use wasm_bindgen::JsCast;
15
16 static IOS_WORKAROUND: RwLock<bool> = RwLock::new(false);
17}}
18
19pub fn on_click_outside<El, M, F>(target: El, handler: F) -> impl FnOnce() + Clone + Send + Sync
90where
91 El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
92 F: FnMut(web_sys::Event) + Clone + 'static,
93{
94 on_click_outside_with_options(target, handler, OnClickOutsideOptions::default())
95}
96
97#[cfg_attr(feature = "ssr", allow(unused_variables))]
99pub fn on_click_outside_with_options<El, M, F>(
100 target: El,
101 handler: F,
102 options: OnClickOutsideOptions,
103) -> impl FnOnce() + Clone + Send + Sync
104where
105 El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
106 F: FnMut(web_sys::Event) + Clone + 'static,
107{
108 #[cfg(feature = "ssr")]
109 {
110 || {}
111 }
112
113 #[cfg(not(feature = "ssr"))]
114 {
115 let OnClickOutsideOptions {
116 ignore,
117 capture,
118 detect_iframes,
119 } = options;
120
121 if *IS_IOS
124 && let Ok(mut ios_workaround) = IOS_WORKAROUND.write()
125 && !*ios_workaround
126 {
127 *ios_workaround = true;
128 if let Some(body) = document().body() {
129 let children = body.children();
130 for i in 0..children.length() {
131 let _ = children
132 .get_with_index(i)
133 .expect("checked index")
134 .add_event_listener_with_callback("click", &js_sys::Function::default());
135 }
136 }
137 }
138
139 let should_listen = Rc::new(Cell::new(true));
140
141 let should_ignore = move |event: &web_sys::UiEvent| {
142 let ignore = ignore.get_untracked();
143
144 ignore.into_iter().flatten().any(|element| {
145 event_target::<web_sys::EventTarget>(event) == element
146 || event.composed_path().includes(element.as_ref(), 0)
147 })
148 };
149
150 let target = target.into_element_maybe_signal();
151
152 let listener = {
153 let should_listen = Rc::clone(&should_listen);
154 let mut handler = handler.clone();
155
156 move |event: web_sys::UiEvent| {
157 if let Some(el) = target.get_untracked() {
158 if el == event_target(&event) || event.composed_path().includes(el.as_ref(), 0)
159 {
160 return;
161 }
162
163 if event.detail() == 0 {
164 should_listen.set(!should_ignore(&event));
165 }
166
167 if !should_listen.get() {
168 should_listen.set(true);
169 return;
170 }
171
172 #[cfg(debug_assertions)]
173 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
174
175 handler(event.into());
176 }
177 }
178 };
179
180 let remove_click_listener = {
181 let mut listener = listener.clone();
182
183 use_event_listener_with_options::<_, web_sys::Window, _, _>(
184 window(),
185 click,
186 move |event| listener(event.into()),
187 UseEventListenerOptions::default()
188 .passive(true)
189 .capture(capture),
190 )
191 };
192
193 let remove_pointer_listener = {
194 let should_listen = Rc::clone(&should_listen);
195
196 use_event_listener_with_options::<_, web_sys::Window, _, _>(
197 window(),
198 pointerdown,
199 move |event| {
200 if let Some(el) = target.get_untracked() {
201 should_listen
202 .set(!event.composed_path().includes(&el, 0) && !should_ignore(&event));
203 }
204 },
205 UseEventListenerOptions::default().passive(true),
206 )
207 };
208
209 let remove_blur_listener = if detect_iframes {
210 Some(use_event_listener::<_, web_sys::Window, _, _>(
211 window(),
212 blur,
213 move |event| {
214 let mut handler = handler.clone();
215
216 let _ = set_timeout_with_handle(
217 move || {
218 if let Some(el) = target.get_untracked()
219 && let Some(active_element) = document().active_element()
220 && active_element.tag_name() == "IFRAME"
221 && !el
222 .unchecked_into::<web_sys::Node>()
223 .contains(Some(&active_element.into()))
224 {
225 handler(event.into());
226 }
227 },
228 Duration::ZERO,
229 );
230 },
231 ))
232 } else {
233 None
234 };
235
236 sendwrap_fn!(once move || {
237 remove_click_listener();
238 remove_pointer_listener();
239 if let Some(f) = remove_blur_listener {
240 f();
241 }
242 })
243 }
244}
245
246#[derive(Clone, DefaultBuilder)]
248#[cfg_attr(feature = "ssr", allow(dead_code))]
249pub struct OnClickOutsideOptions {
250 #[builder(skip)]
252 ignore: ElementsMaybeSignal<web_sys::EventTarget>,
253
254 capture: bool,
256
257 detect_iframes: bool,
259}
260
261impl Default for OnClickOutsideOptions {
262 fn default() -> Self {
263 Self {
264 ignore: Vec::<web_sys::EventTarget>::new().into_elements_maybe_signal(),
265 capture: true,
266 detect_iframes: false,
267 }
268 }
269}
270
271impl OnClickOutsideOptions {
272 #[cfg_attr(feature = "ssr", allow(dead_code))]
274 pub fn ignore<M>(self, ignore: impl IntoElementsMaybeSignal<web_sys::EventTarget, M>) -> Self {
275 Self {
276 ignore: ignore.into_elements_maybe_signal(),
277 ..self
278 }
279 }
280}