added on_click_outside

This commit is contained in:
Maccesch 2023-06-13 17:48:32 +01:00
parent a6e94af982
commit 3ae4c14ca5
31 changed files with 1106 additions and 305 deletions

2
.idea/leptos-use.iml generated
View file

@ -29,6 +29,8 @@
<sourceFolder url="file://$MODULE_DIR$/examples/use_intersection_observer/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_round/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_mutation_observer/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/on_click_outside/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_abs/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/examples/use_event_listener/target" />
<excludeFolder url="file://$MODULE_DIR$/target" />
<excludeFolder url="file://$MODULE_DIR$/docs/book/book" />

View file

@ -3,7 +3,7 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.3.0] - 2023-06-13
### Braking Changes 🛠
- `use_event_listener` no longer returns a `Box<dyn Fn()>` but a `impl Fn() + Clone`
@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `whenever`
- `use_mutation_observer`
- `use_abs`
- `on_click_outside`
## [0.2.1] - 2023-06-11

View file

@ -22,6 +22,7 @@ num = { version = "0.4.0", optional = true }
serde = { version = "1.0.163", optional = true }
serde_json = { version = "1.0.96", optional = true }
paste = "1.0.12"
lazy_static = "1.4.0"
[dependencies.web-sys]
version = "0.3.63"

View file

@ -12,7 +12,7 @@
<p align="center">
<a href="https://crates.io/crates/leptos-use"><img src="https://img.shields.io/crates/v/leptos-use.svg?label=&color=%232C1275" alt="Crates.io"/></a>
<a href="https://leptos-use.rs"><img src="https://img.shields.io/badge/-docs%20%26%20demos-%239A233F" alt="Docs & Demos"></a>
<a href="https://leptos-use.rs"><img src="https://img.shields.io/badge/-27%20functions-%23EF3939" alt="27 Functions" /></a>
<a href="https://leptos-use.rs"><img src="https://img.shields.io/badge/-31%20functions-%23EF3939" alt="31 Functions" /></a>
</p>
<br/>

View file

@ -30,6 +30,7 @@
# Sensors
- [on_click_outside](sensors/on_click_outside.md)
- [use_mouse](sensors/use_mouse.md)
- [use_scroll](sensors/use_scroll.md)

View file

@ -44,6 +44,11 @@
vertical-align: middle;
}
.demo-container button.small {
border-bottom-width: 1px;
padding: 1px 10px;
}
.demo-container button:hover:not(:disabled) {
background-color: var(--brand-color-dark);
}

View file

@ -11,6 +11,6 @@
<p>
<a href="https://crates.io/crates/leptos-use"><img src="https://img.shields.io/crates/v/leptos-use.svg?label=&color=%232C1275" alt="Crates.io"/></a>
<a href="./get_started.html"><img src="https://img.shields.io/badge/-docs%20%26%20demos-%239A233F" alt="Docs & Demos"></a>
<a href="./functions.html"><img src="https://img.shields.io/badge/-27%20functions-%23EF3939" alt="27 Functions" /></a>
<a href="./functions.html"><img src="https://img.shields.io/badge/-31%20functions-%23EF3939" alt="31 Functions" /></a>
</p>
</div>

View file

@ -0,0 +1,3 @@
# on_click_outside
<!-- cmdrun python3 ../extract_doc_comment.py on_click_outside -->

View file

@ -1,6 +1,7 @@
[workspace]
members = [
"on_click_outside",
"use_abs",
"use_breakpoints",
"use_ceil",

View file

@ -0,0 +1,16 @@
[package]
name = "on_click_outside"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.3"
console_error_panic_hook = "0.1"
console_log = "1"
log = "0.4"
leptos-use = { path = "../..", features = ["docs"] }
web-sys = "0.3"
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"

View file

@ -0,0 +1,23 @@
A simple example for `on_click_outside`.
If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation)
as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target:
```bash
cargo install trunk
npm install -D tailwindcss @tailwindcss/forms
rustup toolchain install nightly
rustup target add wasm32-unknown-unknown
```
Then, open two terminals. In the first one, run:
```
npx tailwindcss -i ./input.css -o ./style/output.css --watch
```
In the second one, run:
```bash
trunk serve --open
```

View file

@ -0,0 +1,2 @@
[build]
public_url = "/demo/"

View file

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="css" href="style/output.css">
</head>
<body></body>
</html>

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View file

@ -0,0 +1,75 @@
use leptos::*;
use leptos_use::docs::demo_or_body;
use leptos_use::on_click_outside;
#[component]
fn Demo(cx: Scope) -> impl IntoView {
let (show_modal, set_show_modal) = create_signal(cx, false);
let modal_ref = create_node_ref(cx);
let _ = on_click_outside(cx, modal_ref, move |_| set_show_modal(false));
view! { cx,
<button on:click=move |_| set_show_modal(true)>"Open Modal"</button>
<Show when=show_modal fallback=|_| view! { cx, }>
<div node_ref=modal_ref class="modal">
<div class="inner">
<button class="button small" title="Close" on:click=move |_| set_show_modal(false)>"𝖷"</button>
<p class="heading">"Demo Modal"</p>
<p>"Click outside this modal to close it."</p>
</div>
</div>
</Show>
<style>"
.modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 420px;
max-width: 100%;
z-index: 10;
}
.inner {
background-color: var(--bg);
padding: 0.4em 2em;
border-radius: 5px;
border: 1px solid var(--theme-popup-border);
box-shadow: 2px 2px 10px rgba(10, 10, 10, 0.1);
}
.dropdown-inner {
background-color: var(--bg);
padding: 0.5em;
position: absolute;
left: 0;
z-index: 10;
border-radius: 5px;
border: 1px solid var(--theme-popup-border);
box-shadow: 2px 2px 5px rgba(10, 10, 10, 0.1);
}
.heading {
font-weight: bold;
font-size: 1.4rem;
margin-bottom: 2rem;
}
.modal > .inner > .button {
position: absolute;
top: 0;
right: 0;
margin: 0;
font-weight: bold;
}
"</style>
}
}
fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to(demo_or_body(), |cx| {
view! { cx, <Demo /> }
})
}

View file

@ -0,0 +1,305 @@
[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
border-radius: 0px;
padding-top: 0.5rem;
padding-right: 0.75rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
font-size: 1rem;
line-height: 1.5rem;
--tw-shadow: 0 0 #0000;
}
[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
border-color: #2563eb;
}
input::-moz-placeholder, textarea::-moz-placeholder {
color: #6b7280;
opacity: 1;
}
input::placeholder,textarea::placeholder {
color: #6b7280;
opacity: 1;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-date-and-time-value {
min-height: 1.5em;
}
::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
padding-top: 0;
padding-bottom: 0;
}
select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
[multiple] {
background-image: initial;
background-position: initial;
background-repeat: unset;
background-size: initial;
padding-right: 0.75rem;
-webkit-print-color-adjust: unset;
print-color-adjust: unset;
}
[type='checkbox'],[type='radio'] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
padding: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
display: inline-block;
vertical-align: middle;
background-origin: border-box;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
flex-shrink: 0;
height: 1rem;
width: 1rem;
color: #2563eb;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
--tw-shadow: 0 0 #0000;
}
[type='checkbox'] {
border-radius: 0px;
}
[type='radio'] {
border-radius: 100%;
}
[type='checkbox']:focus,[type='radio']:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 2px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
[type='checkbox']:checked,[type='radio']:checked {
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
[type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
[type='radio']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
}
[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
border-color: transparent;
background-color: currentColor;
}
[type='checkbox']:indeterminate {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
border-color: transparent;
background-color: currentColor;
}
[type='file'] {
background: unset;
border-color: inherit;
border-width: 0;
border-radius: 0;
padding: 0;
font-size: unset;
line-height: inherit;
}
[type='file']:focus {
outline: 1px solid ButtonText;
outline: 1px auto -webkit-focus-ring-color;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.static {
position: static;
}
.fixed {
position: fixed;
}
.absolute {
position: absolute;
}
.transform {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.border {
border-width: 1px;
}
.text-\[--brand-color\] {
color: var(--brand-color);
}
.text-green-600 {
--tw-text-opacity: 1;
color: rgb(22 163 74 / var(--tw-text-opacity));
}
.opacity-75 {
opacity: 0.75;
}
@media (prefers-color-scheme: dark) {
.dark\:text-green-500 {
--tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity));
}
}

View file

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"],
},
theme: {
extend: {},
},
corePlugins: {
preflight: false,
},
plugins: [
require('@tailwindcss/forms'),
],
}

View file

@ -20,6 +20,7 @@ pub use use_element_size::*;
#[cfg(web_sys_unstable_apis)]
pub use use_resize_observer::*;
mod on_click_outside;
mod use_breakpoints;
mod use_debounce_fn;
mod use_element_visibility;
@ -40,6 +41,7 @@ mod watch_pausable;
mod watch_throttled;
mod whenever;
pub use on_click_outside::*;
pub use use_breakpoints::*;
pub use use_debounce_fn::*;
pub use use_element_visibility::*;

253
src/on_click_outside.rs Normal file
View file

@ -0,0 +1,253 @@
use crate::core::{ElementMaybeSignal, ElementsMaybeSignal};
use crate::utils::IS_IOS;
use crate::{use_event_listener, use_event_listener_with_options};
use default_struct_builder::DefaultBuilder;
use leptos::ev::{blur, click, pointerdown};
use leptos::*;
use std::cell::Cell;
use std::rc::Rc;
use std::sync::RwLock;
use std::time::Duration;
use wasm_bindgen::JsCast;
use web_sys::AddEventListenerOptions;
static IOS_WORKAROUND: RwLock<bool> = RwLock::new(false);
/// Listen for clicks outside of an element.
/// Useful for modals or dropdowns.
///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/on_click_outside)
///
/// ## Usage
///
/// ```
/// # use leptos::*;
/// # use leptos::ev::resize;
/// # use leptos_use::on_click_outside;
/// #
/// # #[component]
/// # fn Demo(cx: Scope) -> impl IntoView {
/// let target = create_node_ref(cx);
///
/// on_click_outside(cx, target, move |event| { log!("{:?}", event); });
///
/// view! { cx,
/// <div node_ref=target>"Hello World"</div>
/// <div>"Outside element"</div>
/// }
/// # }
/// ```
///
/// > This function uses [Event.composedPath()](https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath)
/// which is NOT supported by IE 11, Edge 18 and below.
/// If you are targeting these browsers, we recommend you to include
/// [this code snippet](https://gist.github.com/sibbng/13e83b1dd1b733317ce0130ef07d4efd) on your project.
pub fn on_click_outside<El, T, F>(cx: Scope, target: El, handler: F) -> impl FnOnce() + Clone
where
El: Clone,
(Scope, El): Into<ElementMaybeSignal<T, web_sys::EventTarget>>,
T: Into<web_sys::EventTarget> + Clone + 'static,
F: FnMut(web_sys::Event) + Clone + 'static,
{
on_click_outside_with_options::<_, _, _, web_sys::EventTarget>(
cx,
target,
handler,
OnClickOutsideOptions::default(),
)
}
/// Version of `on_click_outside` that takes an `OnClickOutsideOptions`. See `on_click_outside` for more details.
pub fn on_click_outside_with_options<El, T, F, I>(
cx: Scope,
target: El,
handler: F,
options: OnClickOutsideOptions<I>,
) -> impl FnOnce() + Clone
where
El: Clone,
(Scope, El): Into<ElementMaybeSignal<T, web_sys::EventTarget>>,
T: Into<web_sys::EventTarget> + Clone + 'static,
F: FnMut(web_sys::Event) + Clone + 'static,
I: Into<web_sys::EventTarget> + Clone + 'static,
{
let OnClickOutsideOptions {
ignore,
capture,
detect_iframes,
} = options;
// Fixes: https://github.com/vueuse/vueuse/issues/1520
// How it works: https://stackoverflow.com/a/39712411
if *IS_IOS {
if let Ok(mut ios_workaround) = IOS_WORKAROUND.write() {
if !*ios_workaround {
*ios_workaround = true;
if let Some(body) = document().body() {
let children = body.children();
for i in 0..children.length() {
let _ = children
.get_with_index(i)
.expect("checked index")
.add_event_listener_with_callback(
"click",
&js_sys::Function::default(),
);
}
}
}
}
}
let should_listen = Rc::new(Cell::new(true));
let should_ignore = move |event: &web_sys::UiEvent| {
let ignore = ignore.get_untracked();
ignore.into_iter().flatten().any(|element| {
let element: web_sys::EventTarget = element.into();
event_target::<web_sys::EventTarget>(event) == element
|| event.composed_path().includes(element.as_ref(), 0)
})
};
let target = (cx, target).into();
let listener = {
let should_listen = Rc::clone(&should_listen);
let mut handler = handler.clone();
let target = target.clone();
let should_ignore = should_ignore.clone();
move |event: web_sys::UiEvent| {
if let Some(el) = target.get_untracked() {
let el = el.into();
if el == event_target(&event) || event.composed_path().includes(el.as_ref(), 0) {
return;
}
if event.detail() == 0 {
should_listen.set(!should_ignore(&event));
}
if !should_listen.get() {
should_listen.set(true);
return;
}
handler(event.into());
}
}
};
let remove_click_listener = {
let mut listener = listener.clone();
let mut options = AddEventListenerOptions::default();
options.passive(true).capture(capture);
use_event_listener_with_options::<_, web_sys::Window, _, _>(
cx,
window(),
click,
move |event| listener(event.into()),
options,
)
};
let remove_pointer_listener = {
let target = target.clone();
let should_listen = Rc::clone(&should_listen);
let mut options = AddEventListenerOptions::default();
options.passive(true);
use_event_listener_with_options::<_, web_sys::Window, _, _>(
cx,
window(),
pointerdown,
move |event| {
if let Some(el) = target.get_untracked() {
should_listen.set(
!event.composed_path().includes(el.into().as_ref(), 0)
&& !should_ignore(&event),
);
}
},
options,
)
};
let remove_blur_listener = if detect_iframes {
Some(use_event_listener::<_, web_sys::Window, _, _>(
cx,
window(),
blur,
move |event| {
let target = target.clone();
let mut handler = handler.clone();
let _ = set_timeout_with_handle(
move || {
if let Some(el) = target.get_untracked() {
if let Some(active_element) = document().active_element() {
if active_element.tag_name() == "IFRAME"
&& !el
.into()
.unchecked_into::<web_sys::Node>()
.contains(Some(&active_element.into()))
{
handler(event.into());
}
}
}
},
Duration::ZERO,
);
},
))
} else {
None
};
move || {
remove_click_listener();
remove_pointer_listener();
if let Some(f) = remove_blur_listener {
f();
}
}
}
/// Options for [`on_click_outside_with_options`].
#[derive(Clone, DefaultBuilder)]
pub struct OnClickOutsideOptions<T>
where
T: Into<web_sys::EventTarget> + Clone + 'static,
{
/// List of elementss that should not trigger the callback. Defaults to `[]`.
ignore: ElementsMaybeSignal<T, web_sys::EventTarget>,
/// Use capturing phase for internal event listener. Defaults to `true`.
capture: bool,
/// Run callback if focus moves to an iframe. Defaults to `false`.
detect_iframes: bool,
}
impl<T> Default for OnClickOutsideOptions<T>
where
T: Into<web_sys::EventTarget> + Clone + 'static,
{
fn default() -> Self {
Self {
ignore: Default::default(),
capture: true,
detect_iframes: false,
}
}
}

View file

@ -66,82 +66,88 @@ where
let box_ = options.box_;
let initial_size = options.initial_size;
let targ = (cx, target).into();
let target = (cx, target).into();
let t = targ.clone();
let is_svg = move || {
if let Some(target) = t.get_untracked() {
target
.into()
.namespace_uri()
.map(|ns| ns.contains("svg"))
.unwrap_or(false)
} else {
false
let is_svg = {
let target = target.clone();
move || {
if let Some(target) = target.get_untracked() {
target
.into()
.namespace_uri()
.map(|ns| ns.contains("svg"))
.unwrap_or(false)
} else {
false
}
}
};
let (width, set_width) = create_signal(cx, options.initial_size.width);
let (height, set_height) = create_signal(cx, options.initial_size.height);
let t = targ.clone();
let _ = use_resize_observer_with_options::<ElementMaybeSignal<T, web_sys::Element>, _, _>(
cx,
targ.clone(),
move |entries, _| {
let entry = &entries[0];
{
let target = target.clone();
let box_size = match box_ {
web_sys::ResizeObserverBoxOptions::ContentBox => entry.content_box_size(),
web_sys::ResizeObserverBoxOptions::BorderBox => entry.border_box_size(),
web_sys::ResizeObserverBoxOptions::DevicePixelContentBox => {
entry.device_pixel_content_box_size()
}
_ => unreachable!(),
};
let _ = use_resize_observer_with_options::<ElementMaybeSignal<T, web_sys::Element>, _, _>(
cx,
target.clone(),
move |entries, _| {
let entry = &entries[0];
if is_svg() {
if let Some(target) = t.get() {
if let Ok(Some(styles)) = window.get_computed_style(&target.into()) {
set_height(
styles
.get_property_value("height")
.map(|v| v.parse().unwrap_or_default())
.unwrap_or_default(),
);
set_width(
styles
.get_property_value("width")
.map(|v| v.parse().unwrap_or_default())
.unwrap_or_default(),
);
let box_size = match box_ {
web_sys::ResizeObserverBoxOptions::ContentBox => entry.content_box_size(),
web_sys::ResizeObserverBoxOptions::BorderBox => entry.border_box_size(),
web_sys::ResizeObserverBoxOptions::DevicePixelContentBox => {
entry.device_pixel_content_box_size()
}
}
} else if !box_size.is_null() && !box_size.is_undefined() && box_size.length() > 0 {
let format_box_size = if box_size.is_array() {
box_size.to_vec()
} else {
vec![box_size.into()]
_ => unreachable!(),
};
set_width(format_box_size.iter().fold(0.0, |acc, v| {
acc + v.as_ref().clone().unchecked_into::<BoxSize>().inline_size()
}));
set_height(format_box_size.iter().fold(0.0, |acc, v| {
acc + v.as_ref().clone().unchecked_into::<BoxSize>().block_size()
}))
} else {
// fallback
set_width(entry.content_rect().width());
set_height(entry.content_rect().height())
}
},
options.into(),
);
if is_svg() {
if let Some(target) = target.get() {
if let Ok(Some(styles)) = window.get_computed_style(&target.into()) {
set_height(
styles
.get_property_value("height")
.map(|v| v.parse().unwrap_or_default())
.unwrap_or_default(),
);
set_width(
styles
.get_property_value("width")
.map(|v| v.parse().unwrap_or_default())
.unwrap_or_default(),
);
}
}
} else if !box_size.is_null() && !box_size.is_undefined() && box_size.length() > 0 {
let format_box_size = if box_size.is_array() {
box_size.to_vec()
} else {
vec![box_size.into()]
};
set_width(format_box_size.iter().fold(0.0, |acc, v| {
acc + v.as_ref().clone().unchecked_into::<BoxSize>().inline_size()
}));
set_height(format_box_size.iter().fold(0.0, |acc, v| {
acc + v.as_ref().clone().unchecked_into::<BoxSize>().block_size()
}))
} else {
// fallback
set_width(entry.content_rect().width());
set_height(entry.content_rect().height())
}
},
options.into(),
);
}
let _ = watch_with_options(
cx,
move || targ.get(),
move || target.get(),
move |ele, _, _| {
if ele.is_some() {
set_width(initial_size.width);

View file

@ -119,10 +119,15 @@ where
let event_name = event.name();
let closure_js = Closure::wrap(Box::new(handler) as Box<dyn FnMut(_)>).into_js_value();
let closure = closure_js.clone();
let cleanup_fn = move |element: &web_sys::EventTarget| {
let _ = element
.remove_event_listener_with_callback(&event_name, closure.as_ref().unchecked_ref());
let cleanup_fn = {
let closure_js = closure_js.clone();
move |element: &web_sys::EventTarget| {
let _ = element.remove_event_listener_with_callback(
&event_name,
closure_js.as_ref().unchecked_ref(),
);
}
};
let event_name = event.name();
@ -132,33 +137,37 @@ where
let prev_element: Rc<RefCell<Option<web_sys::EventTarget>>> =
Rc::new(RefCell::new(signal.get_untracked().map(|e| e.into())));
let prev_el = prev_element.clone();
let cleanup_prev_element = move || {
if let Some(element) = prev_el.take() {
cleanup_fn(&element);
let cleanup_prev_element = {
let prev_element = prev_element.clone();
move || {
if let Some(element) = prev_element.take() {
cleanup_fn(&element);
}
}
};
let cleanup_prev_el = cleanup_prev_element.clone();
let closure = closure_js;
let stop_watch = {
let cleanup_prev_element = cleanup_prev_element.clone();
let stop_watch = watch_with_options(
cx,
move || signal.get().map(|e| e.into()),
move |element, _, _| {
cleanup_prev_el();
prev_element.replace(element.clone());
watch_with_options(
cx,
move || signal.get().map(|e| e.into()),
move |element, _, _| {
cleanup_prev_element();
prev_element.replace(element.clone());
if let Some(element) = element {
_ = element.add_event_listener_with_callback_and_add_event_listener_options(
&event_name,
closure.as_ref().unchecked_ref(),
&options,
);
}
},
WatchOptions::default().immediate(true),
);
if let Some(element) = element {
_ = element.add_event_listener_with_callback_and_add_event_listener_options(
&event_name,
closure_js.as_ref().unchecked_ref(),
&options,
);
}
},
WatchOptions::default().immediate(true),
)
};
let stop = move || {
stop_watch();

View file

@ -103,82 +103,94 @@ where
let observer: Rc<RefCell<Option<web_sys::IntersectionObserver>>> = Rc::new(RefCell::new(None));
let obs = Rc::clone(&observer);
let cleanup = move || {
if let Some(o) = obs.take() {
o.disconnect();
let cleanup = {
let obsserver = Rc::clone(&observer);
move || {
if let Some(o) = obsserver.take() {
o.disconnect();
}
}
};
let targets = (cx, target).into();
let root = root.map(|root| (cx, root).into());
let clean = cleanup.clone();
let stop_watch = watch_with_options(
cx,
let stop_watch = {
let cleanup = cleanup.clone();
watch_with_options(
cx,
move || {
(
targets.get(),
root.as_ref().map(|root| root.get()),
is_active.get(),
)
},
move |values, _, _| {
let (targets, root, is_active) = values;
cleanup();
if !is_active {
return;
}
let mut options = web_sys::IntersectionObserverInit::new();
options.root_margin(&root_margin).threshold(
&thresholds
.iter()
.copied()
.map(JsValue::from)
.collect::<js_sys::Array>(),
);
if let Some(Some(root)) = root {
let root: web_sys::Element = root.clone().into();
options.root(Some(&root));
}
let obs = web_sys::IntersectionObserver::new_with_options(
closure_js.clone().as_ref().unchecked_ref(),
&options,
)
.expect("failed to create IntersectionObserver");
for target in targets.iter().flatten() {
let target: web_sys::Element = target.clone().into();
obs.observe(&target);
}
observer.replace(Some(obs));
},
WatchOptions::default().immediate(immediate),
)
};
let stop = {
let cleanup = cleanup.clone();
move || {
(
targets.get(),
root.as_ref().map(|root| root.get()),
is_active.get(),
)
},
move |values, _, _| {
let (targets, root, is_active) = values;
clean();
if !is_active {
return;
}
let mut options = web_sys::IntersectionObserverInit::new();
options.root_margin(&root_margin).threshold(
&thresholds
.iter()
.copied()
.map(JsValue::from)
.collect::<js_sys::Array>(),
);
if let Some(Some(root)) = root {
let root: web_sys::Element = root.clone().into();
options.root(Some(&root));
}
let obs = web_sys::IntersectionObserver::new_with_options(
closure_js.clone().as_ref().unchecked_ref(),
&options,
)
.expect("failed to create IntersectionObserver");
for target in targets.iter().flatten() {
let target: web_sys::Element = target.clone().into();
obs.observe(&target);
}
observer.replace(Some(obs));
},
WatchOptions::default().immediate(immediate),
);
let clean = cleanup.clone();
let stop = move || {
clean();
stop_watch();
cleanup();
stop_watch();
}
};
on_cleanup(cx, stop.clone());
let clean = cleanup.clone();
let pause = {
let cleanup = cleanup.clone();
move || {
cleanup();
set_active(false);
}
};
UseIntersectionObserverReturn {
is_active: is_active.into(),
pause: move || {
clean();
set_active(false);
},
pause,
resume: move || {
cleanup();
set_active(true);

View file

@ -40,47 +40,53 @@ pub fn use_media_query(cx: Scope, query: impl Into<MaybeSignal<String>>) -> Sign
let media_query: Rc<RefCell<Option<web_sys::MediaQueryList>>> = Rc::new(RefCell::new(None));
let remove_listener: RemoveListener = Rc::new(RefCell::new(None));
let rem_listener = Rc::clone(&remove_listener);
let listener: Rc<OnceCell<Box<dyn CloneableFnMutWithArg<web_sys::Event>>>> =
Rc::new(OnceCell::new());
let cleanup = move || {
if let Some(remove_listener) = rem_listener.take().as_ref() {
remove_listener();
let cleanup = {
let remove_listener = Rc::clone(&remove_listener);
move || {
if let Some(remove_listener) = remove_listener.take().as_ref() {
remove_listener();
}
}
};
let clean = cleanup.clone();
let listen = Rc::clone(&listener);
let update = {
let cleanup = cleanup.clone();
let listener = Rc::clone(&listener);
let update = move || {
clean();
move || {
cleanup();
let mut media_query = media_query.borrow_mut();
*media_query = window().match_media(&query.get()).unwrap_or(None);
let mut media_query = media_query.borrow_mut();
*media_query = window().match_media(&query.get()).unwrap_or(None);
if let Some(media_query) = media_query.as_ref() {
set_matches(media_query.matches());
if let Some(media_query) = media_query.as_ref() {
set_matches(media_query.matches());
remove_listener.replace(Some(Box::new(use_event_listener(
cx,
media_query.clone(),
change,
listen
.get()
.expect("cell should be initialized by now")
.clone(),
))));
} else {
set_matches(false);
remove_listener.replace(Some(Box::new(use_event_listener(
cx,
media_query.clone(),
change,
listener
.get()
.expect("cell should be initialized by now")
.clone(),
))));
} else {
set_matches(false);
}
}
};
let upd = update.clone();
listener
.set(Box::new(move |_| upd()) as Box<dyn CloneableFnMutWithArg<web_sys::Event>>)
.expect("cell is empty");
{
let update = update.clone();
listener
.set(Box::new(move |_| update()) as Box<dyn CloneableFnMutWithArg<web_sys::Event>>)
.expect("cell is empty");
}
create_effect(cx, move |_| update());

View file

@ -101,37 +101,46 @@ where
let (y, set_y) = create_signal(cx, options.initial_value.y);
let (source_type, set_source_type) = create_signal(cx, UseMouseSourceType::Unset);
let coord_type = options.coord_type.clone();
let mouse_handler = move |event: web_sys::MouseEvent| {
let result = coord_type.extract_mouse_coords(&event);
let mouse_handler = {
let coord_type = options.coord_type.clone();
if let Some((x, y)) = result {
set_x(x);
set_y(y);
set_source_type(UseMouseSourceType::Mouse);
}
};
let handler = mouse_handler.clone();
let drag_handler = move |event: web_sys::DragEvent| {
let js_value: &JsValue = event.as_ref();
handler(js_value.clone().unchecked_into::<web_sys::MouseEvent>());
};
let coord_type = options.coord_type.clone();
let touch_handler = move |event: web_sys::TouchEvent| {
let touches = event.touches();
if touches.length() > 0 {
let result = coord_type.extract_touch_coords(
&touches
.get(0)
.expect("Just checked that there's at least on touch"),
);
move |event: web_sys::MouseEvent| {
let result = coord_type.extract_mouse_coords(&event);
if let Some((x, y)) = result {
set_x(x);
set_y(y);
set_source_type(UseMouseSourceType::Touch);
set_source_type(UseMouseSourceType::Mouse);
}
}
};
let drag_handler = {
let mouse_handler = mouse_handler.clone();
move |event: web_sys::DragEvent| {
let js_value: &JsValue = event.as_ref();
mouse_handler(js_value.clone().unchecked_into::<web_sys::MouseEvent>());
}
};
let touch_handler = {
let coord_type = options.coord_type.clone();
move |event: web_sys::TouchEvent| {
let touches = event.touches();
if touches.length() > 0 {
let result = coord_type.extract_touch_coords(
&touches
.get(0)
.expect("Just checked that there's at least on touch"),
);
if let Some((x, y)) = result {
set_x(x);
set_y(y);
set_source_type(UseMouseSourceType::Touch);
}
}
}
};

View file

@ -87,37 +87,43 @@ where
let is_supported = use_supported(cx, || JsValue::from("MutationObserver").js_in(&window()));
let obs = Rc::clone(&observer);
let cleanup = move || {
let mut observer = obs.borrow_mut();
if let Some(o) = observer.as_ref() {
o.disconnect();
*observer = None;
let cleanup = {
let observer = Rc::clone(&observer);
move || {
let mut observer = observer.borrow_mut();
if let Some(o) = observer.as_ref() {
o.disconnect();
*observer = None;
}
}
};
let targets = (cx, target).into();
let clean = cleanup.clone();
let stop_watch = watch(
cx,
move || targets.get(),
move |targets, _, _| {
clean();
let stop_watch = {
let cleanup = cleanup.clone();
if is_supported() && !targets.is_empty() {
let obs = web_sys::MutationObserver::new(closure_js.as_ref().unchecked_ref())
.expect("failed to create MutationObserver");
watch(
cx,
move || targets.get(),
move |targets, _, _| {
cleanup();
for target in targets.iter().flatten() {
let target: web_sys::Element = target.clone().into();
let _ = obs.observe_with_options(&target, &options.clone());
if is_supported() && !targets.is_empty() {
let obs = web_sys::MutationObserver::new(closure_js.as_ref().unchecked_ref())
.expect("failed to create MutationObserver");
for target in targets.iter().flatten() {
let target: web_sys::Element = target.clone().into();
let _ = obs.observe_with_options(&target, &options.clone());
}
observer.replace(Some(obs));
}
observer.replace(Some(obs));
}
},
);
},
)
};
let stop = move || {
cleanup();

View file

@ -89,37 +89,43 @@ where
let is_supported = use_supported(cx, || JsValue::from("ResizeObserver").js_in(&window()));
let obs = Rc::clone(&observer);
let cleanup = move || {
let mut observer = obs.borrow_mut();
if let Some(o) = observer.as_ref() {
o.disconnect();
*observer = None;
let cleanup = {
let observer = Rc::clone(&observer);
move || {
let mut observer = observer.borrow_mut();
if let Some(o) = observer.as_ref() {
o.disconnect();
*observer = None;
}
}
};
let targets = (cx, target).into();
let clean = cleanup.clone();
let stop_watch = watch(
cx,
move || targets.get(),
move |targets, _, _| {
clean();
let stop_watch = {
let cleanup = cleanup.clone();
if is_supported() && !targets.is_empty() {
let obs = web_sys::ResizeObserver::new(closure_js.clone().as_ref().unchecked_ref())
.expect("failed to create ResizeObserver");
watch(
cx,
move || targets.get(),
move |targets, _, _| {
cleanup();
for target in targets.iter().flatten() {
let target: web_sys::Element = target.clone().into();
obs.observe_with_options(&target, &options.clone().into());
if is_supported() && !targets.is_empty() {
let obs = web_sys::ResizeObserver::new(closure_js.clone().as_ref().unchecked_ref())
.expect("failed to create ResizeObserver");
for target in targets.iter().flatten() {
let target: web_sys::Element = target.clone().into();
obs.observe_with_options(&target, &options.clone().into());
}
observer.replace(Some(obs));
}
observer.replace(Some(obs));
}
},
);
},
)
};
let stop = move || {
cleanup();

View file

@ -186,29 +186,34 @@ where
let signal = (cx, element).into();
let behavior = options.behavior;
let sig = signal.clone();
let scroll_to = move |x: Option<f64>, y: Option<f64>| {
let element = sig.get_untracked();
let scroll_to = {
let signal = signal.clone();
if let Some(element) = element {
let element = element.into();
move |x: Option<f64>, y: Option<f64>| {
let element = signal.get_untracked();
let mut scroll_options = web_sys::ScrollToOptions::new();
scroll_options.behavior(behavior.get_untracked().into());
if let Some(element) = element {
let element = element.into();
if let Some(x) = x {
scroll_options.left(x);
let mut scroll_options = web_sys::ScrollToOptions::new();
scroll_options.behavior(behavior.get_untracked().into());
if let Some(x) = x {
scroll_options.left(x);
}
if let Some(y) = y {
scroll_options.top(y);
}
element.scroll_to_with_scroll_to_options(&scroll_options);
}
if let Some(y) = y {
scroll_options.top(y);
}
element.scroll_to_with_scroll_to_options(&scroll_options);
}
};
let scroll = scroll_to.clone();
let set_x = Box::new(move |x| scroll(Some(x), None));
let set_x = {
let scroll_to = scroll_to.clone();
Box::new(move |x| scroll_to(Some(x), None))
};
let set_y = Box::new(move |y| scroll_to(None, Some(y)));
@ -233,21 +238,25 @@ where
},
);
let on_stop = options.on_stop.clone();
let on_scroll_end = move |e| {
if !is_scrolling.get_untracked() {
return;
}
let on_scroll_end = {
let on_stop = options.on_stop.clone();
set_is_scrolling(false);
directions.update(|directions| {
directions.left = false;
directions.right = false;
directions.top = false;
directions.bottom = false;
on_stop.clone()(e);
});
move |e| {
if !is_scrolling.get_untracked() {
return;
}
set_is_scrolling(false);
directions.update(|directions| {
directions.left = false;
directions.right = false;
directions.top = false;
directions.bottom = false;
on_stop.clone()(e);
});
}
};
let throttle = options.throttle;
let on_scroll_end_debounced =
@ -325,22 +334,27 @@ where
}
};
let on_scroll = options.on_scroll.clone();
let on_scroll_handler = {
let on_scroll = options.on_scroll.clone();
let on_scroll_handler = move |e: web_sys::Event| {
let target: web_sys::Element = event_target(&e);
move |e: web_sys::Event| {
let target: web_sys::Element = event_target(&e);
set_arrived_state(target);
set_is_scrolling(true);
on_scroll_end_debounced.clone()(e.clone());
on_scroll.clone()(e);
set_arrived_state(target);
set_is_scrolling(true);
on_scroll_end_debounced.clone()(e.clone());
on_scroll.clone()(e);
}
};
let sig = signal.clone();
let target = Signal::derive(cx, move || {
let element = sig.get();
element.map(|element| element.into().unchecked_into::<web_sys::EventTarget>())
});
let target = {
let signal = signal.clone();
Signal::derive(cx, move || {
let element = signal.get();
element.map(|element| element.into().unchecked_into::<web_sys::EventTarget>())
})
};
if throttle >= 0.0 {
let throttled_scroll_handler = use_throttle_fn_with_arg_and_options(

10
src/utils/is.rs Normal file
View file

@ -0,0 +1,10 @@
use lazy_static::lazy_static;
use leptos::*;
lazy_static! {
pub static ref IS_IOS: bool = if let Ok(user_agent) = window().navigator().user_agent() {
user_agent.contains("iPhone") || user_agent.contains("iPad") || user_agent.contains("iPod")
} else {
false
};
}

View file

@ -1,5 +1,7 @@
mod clonable_fn;
mod filters;
mod is;
pub use clonable_fn::*;
pub use filters::*;
pub use is::*;

View file

@ -141,18 +141,21 @@ where
let prev_deps_value: Rc<RefCell<Option<W>>> = Rc::new(RefCell::new(None));
let prev_callback_value: Rc<RefCell<Option<T>>> = Rc::new(RefCell::new(None));
let cur_val = Rc::clone(&cur_deps_value);
let prev_val = Rc::clone(&prev_deps_value);
let prev_cb_val = Rc::clone(&prev_callback_value);
let wrapped_callback = move || {
callback(
cur_val
.borrow()
.as_ref()
.expect("this will not be called before there is deps value"),
prev_val.borrow().as_ref(),
prev_cb_val.take(),
)
let wrapped_callback = {
let cur_deps_value = Rc::clone(&cur_deps_value);
let prev_deps_value = Rc::clone(&prev_deps_value);
let prev_cb_val = Rc::clone(&prev_callback_value);
move || {
callback(
cur_deps_value
.borrow()
.as_ref()
.expect("this will not be called before there is deps value"),
prev_deps_value.borrow().as_ref(),
prev_cb_val.take(),
)
}
};
let filtered_callback =