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