Merge branch 'main' into use_webtransport

# Conflicts:
#	CHANGELOG.md
#	Cargo.toml
This commit is contained in:
Maccesch 2023-09-14 13:49:37 +01:00
commit 901bfca95a
56 changed files with 1790 additions and 782 deletions

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

@ -53,6 +53,7 @@
<sourceFolder url="file://$MODULE_DIR$/examples/use_draggable/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_draggable/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_raf_fn/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_raf_fn/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_webtransport/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_webtransport/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_geolocation/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/examples/use_event_listener/target" /> <excludeFolder url="file://$MODULE_DIR$/examples/use_event_listener/target" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
<excludeFolder url="file://$MODULE_DIR$/docs/book/book" /> <excludeFolder url="file://$MODULE_DIR$/docs/book/book" />

6
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.9" />
</component>
</project>

View file

@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### New Functions 🚀 ### New Functions 🚀
- `use_document`
- `use_window`
- `use_geolocation`
- `use_webtransport` - `use_webtransport`
- `signal_debounced` - `signal_debounced`
- `signal_throttled` - `signal_throttled`
@ -16,18 +19,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Leptos version is now 0.5 - Leptos version is now 0.5
- No `cx: Scope` params are supported/needed anymore because of the changes in Leptos. - No `cx: Scope` params are supported/needed anymore because of the changes in Leptos.
Please check the release notes of Leptos 0.5 for how to upgrade. Please check the release notes of Leptos 0.5 for how to upgrade.
- `watch` is now removed in favor of `leptos::watch` and will be removed in a future release. - `watch` is now deprecated in favor of `leptos::watch` and will be removed in a future release.
`watch_with_options` will continue to exist. `watch_with_options` will continue to exist.
- `use_event_listener_with_options` now takes a `UseEventListenerOptions` instead of
a `web_sys::AddEventListenerOptions`.
- `use_mutation_observer_with_options` now takes a `UseMutationObserverOptions` instead of
a `web_sys::MutationObserverInit`.
- `use_websocket`: - `use_websocket`:
- takes now a `&str` instead of a `String` as its `url` parameter. - takes now a `&str` instead of a `String` as its `url` parameter.
- The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`. - The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`.
- The returned signals `ready_state`, `message`, `message_bytes` have now the type - The returned signals `ready_state`, `message`, `message_bytes` have now the type
`Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions. `Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions.
- The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option<u64>` to improve DX. - The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option<u64>` to improve DX.
- The option `manual` has been renamed to `immediate` to make it more consistent with other functions. - The option `manual` has been renamed to `immediate` to make it more consistent with other functions.
To port please note that `immediate` is the inverse of `manual` (`immediate` = `!manual`). To port please note that `immediate` is the inverse of `manual` (`immediate` = `!manual`).
- `use_color_mode`: - `use_color_mode`:
- The optional `on_changed` handler parameters have changed slightly. Please refer to the docs for more details. - The optional `on_changed` handler parameters have changed slightly. Please refer to the docs for more details.
- Throttled or debounced functions cannot be `FnOnce` anymore. - Throttled or debounced functions cannot be `FnOnce` anymore.
- All traits `ClonableFn...` have been removed. - All traits `ClonableFn...` have been removed.
@ -35,7 +42,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Callbacks in options don't require to be cloneable anymore - Callbacks in options don't require to be cloneable anymore
- Callback in `use_raf_fn` doesn't require to be cloneable anymore - Callback in `use_raf_fn` doesn't require to be cloneable anymore
- `use_scroll` is now callable on the server - All (!) functions can now be safely called on the server. Specifically this includes the following that
- panicked on the server:
- `use_scroll`
- `use_event_listener`
- `use_element_hover`
- `on_click_outside`
- `use_drop_zone`
- `use_element_size`
- `use_element_visibility`
- `use_resize_observer`
- `use_intersection_observer`
- `use_mutation_observer`
### Fixes 🍕 ### Fixes 🍕
@ -82,11 +100,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `use_local_storage` - `use_local_storage`
- `use_session_storage` - `use_session_storage`
- Instead of returning `ReadSignal`, the following functions now return `Signal`. - Instead of returning `ReadSignal`, the following functions now return `Signal`.
- `use_color_mode` - `use_color_mode`
- `use_favicon` - `use_favicon`
- `use_storage` - `use_storage`
- `use_local_storage` - `use_local_storage`
- `use_session_storage` - `use_session_storage`
### Fixes 🍕 ### Fixes 🍕

View file

@ -1,6 +1,6 @@
[package] [package]
name = "leptos-use" name = "leptos-use"
version = "0.7.0-beta" version = "0.7.0-rc"
edition = "2021" edition = "2021"
authors = ["Marc-Stefan Cassola"] authors = ["Marc-Stefan Cassola"]
categories = ["gui", "web-programming"] categories = ["gui", "web-programming"]
@ -13,7 +13,7 @@ repository = "https://github.com/Synphonyte/leptos-use"
homepage = "https://leptos-use.rs" homepage = "https://leptos-use.rs"
[dependencies] [dependencies]
leptos = "0.5.0-beta2" leptos = "0.5.0-rc1"
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
js-sys = "0.3" js-sys = "0.3"
default-struct-builder = "0.5" default-struct-builder = "0.5"
@ -33,8 +33,9 @@ version = "0.3"
features = [ features = [
"AddEventListenerOptions", "AddEventListenerOptions",
"BinaryType", "BinaryType",
"CssStyleDeclaration", "Coordinates",
"CloseEvent", "CloseEvent",
"CssStyleDeclaration",
"CustomEvent", "CustomEvent",
"CustomEventInit", "CustomEventInit",
"DomRect", "DomRect",
@ -47,6 +48,7 @@ features = [
"EventTarget", "EventTarget",
"File", "File",
"FileList", "FileList",
"Geolocation",
"HtmlElement", "HtmlElement",
"HtmlLinkElement", "HtmlLinkElement",
"HtmlStyleElement", "HtmlStyleElement",
@ -61,6 +63,9 @@ features = [
"Navigator", "Navigator",
"NodeList", "NodeList",
"PointerEvent", "PointerEvent",
"Position",
"PositionError",
"PositionOptions",
"ReadableStream", "ReadableStream",
"ReadableStreamDefaultReader", "ReadableStreamDefaultReader",
"ReadableStreamGetReaderOptions", "ReadableStreamGetReaderOptions",

View file

@ -90,4 +90,4 @@ To scaffold a new function quickly you can run `template/createfn.sh`. It requir
|---------------|---------------------------| |---------------|---------------------------|
| <= 0.3 | 0.3 | | <= 0.3 | 0.3 |
| 0.4, 0.5, 0.6 | 0.4 | | 0.4, 0.5, 0.6 | 0.4 |
| main | 0.5.0-alpha/beta | | main | 0.5.0-alpha/beta/rc |

View file

@ -15,6 +15,7 @@
# Elements # Elements
- [use_active_element](elements/use_active_element.md) - [use_active_element](elements/use_active_element.md)
- [use_document](elements/use_document.md)
- [use_document_visibility](elements/use_document_visibility.md) - [use_document_visibility](elements/use_document_visibility.md)
- [use_draggable](elements/use_draggable.md) - [use_draggable](elements/use_draggable.md)
- [use_drop_zone](elements/use_drop_zone.md) - [use_drop_zone](elements/use_drop_zone.md)
@ -23,6 +24,7 @@
- [use_intersection_observer](elements/use_intersection_observer.md) - [use_intersection_observer](elements/use_intersection_observer.md)
- [use_mutation_observer](elements/use_mutation_observer.md) - [use_mutation_observer](elements/use_mutation_observer.md)
- [use_resize_observer](elements/use_resize_observer.md) - [use_resize_observer](elements/use_resize_observer.md)
- [use_window](elements/use_window.md)
- [use_window_focus](elements/use_window_focus.md) - [use_window_focus](elements/use_window_focus.md)
- [use_window_scroll](elements/use_window_scroll.md) - [use_window_scroll](elements/use_window_scroll.md)
@ -41,6 +43,7 @@
- [on_click_outside](sensors/on_click_outside.md) - [on_click_outside](sensors/on_click_outside.md)
- [use_element_hover](sensors/use_element_hover.md) - [use_element_hover](sensors/use_element_hover.md)
- [use_geolocation](sensors/use_geolocation.md)
- [use_mouse](sensors/use_mouse.md) - [use_mouse](sensors/use_mouse.md)
- [use_scroll](sensors/use_scroll.md) - [use_scroll](sensors/use_scroll.md)

View file

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

View file

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

View file

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

View file

@ -32,18 +32,23 @@ and the server.
## Functions with Target Elements ## Functions with Target Elements
A lot of functions like `use_event_listener` and `use_element_size` are only useful when a target HTML/SVG element is A lot of functions like `use_resize_observer` and `use_element_size` are only useful when a target HTML/SVG element is
available. This is not the case on the server. You can simply wrap them in `create_effect` which will cause them to available. This is not always the case on the server. If you use them with `NodeRefs` they will just work in SSR.
be only called in the browser. But what if you want to use them with `window()` or `document()`?
To enable that we provide the helper functions [`use_window()`](elements/use_window.md) and [`use_document()`](elements/use_document.md) which return
a new-type-wrapped `Option<web_sys::Window>` or `Option<web_sys::Document>` respectively. These can be
used safely on the server. The following code works on both the client and the server:
```rust ```rust
create_effect( use leptos::*;
cx, use leptos::ev::keyup;
move |_| { use leptos_use::{use_event_listener, use_window};
// window() doesn't work on the server
use_event_listener(window(), "resize", move |_| { use_event_listener(use_window(), keyup, |evt| {
// ... ...
}) });
},
);
``` ```
There are some convenience methods provided as well, like `use_document().body()` which
just propagate a `None` on the server.

View file

@ -22,6 +22,7 @@ members = [
"use_event_listener", "use_event_listener",
"use_favicon", "use_favicon",
"use_floor", "use_floor",
"use_geolocation",
"use_intersection_observer", "use_intersection_observer",
"use_interval", "use_interval",
"use_interval_fn", "use_interval_fn",

View file

@ -5,7 +5,7 @@ use leptos_use::signal_debounced;
#[component] #[component]
fn Demo() -> impl IntoView { fn Demo() -> impl IntoView {
let (input, set_input) = create_signal("".to_string()); let (input, set_input) = create_signal("".to_string());
let debounced = signal_debounced(input, 1000.0); let debounced: Signal<String> = signal_debounced(input, 1000.0);
view! { view! {
<div> <div>

View file

@ -5,7 +5,7 @@ use leptos_use::signal_throttled;
#[component] #[component]
fn Demo() -> impl IntoView { fn Demo() -> impl IntoView {
let (input, set_input) = create_signal("".to_string()); let (input, set_input) = create_signal("".to_string());
let throttled = signal_throttled(input, 1000.0); let throttled: Signal<String> = signal_throttled(input, 1000.0);
view! { view! {
<div> <div>

View file

@ -11,16 +11,16 @@ axum = { version = "0.6.4", optional = true }
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1"
console_log = "1" console_log = "1"
cfg-if = "1" cfg-if = "1"
leptos = { version = "0.5.0-beta2", features = ["nightly"] } leptos = { version = "0.5.0-rc1", features = ["nightly"] }
leptos_axum = { version = "0.5.0-beta2", optional = true } leptos_axum = { version = "0.5.0-rc1", optional = true }
leptos_meta = { version = "0.5.0-beta2", features = ["nightly"] } leptos_meta = { version = "0.5.0-rc1", features = ["nightly"] }
leptos_router = { version = "0.5.0-beta2", features = ["nightly"] } leptos_router = { version = "0.5.0-rc1", features = ["nightly"] }
leptos-use = { path = "../..", features = ["storage"] } leptos-use = { path = "../..", features = ["storage"] }
log = "0.4" log = "0.4"
simple_logger = "4" simple_logger = "4"
tokio = { version = "1.25.0", optional = true } tokio = { version = "1.25.0", optional = true }
tower = { version = "0.4.13", optional = true } tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.0-beta2", features = ["fs"], optional = true } tower-http = { version = "0.4.3", features = ["fs"], optional = true }
wasm-bindgen = "=0.2.87" wasm-bindgen = "=0.2.87"
thiserror = "1.0.38" thiserror = "1.0.38"
tracing = { version = "0.1.37", optional = true } tracing = { version = "0.1.37", optional = true }

View file

@ -5,7 +5,8 @@ use leptos_meta::*;
use leptos_router::*; use leptos_router::*;
use leptos_use::storage::use_local_storage; use leptos_use::storage::use_local_storage;
use leptos_use::{ use leptos_use::{
use_debounce_fn, use_event_listener, use_intl_number_format, UseIntlNumberFormatOptions, use_debounce_fn, use_event_listener, use_intl_number_format, use_window,
UseIntlNumberFormatOptions,
}; };
#[component] #[component]
@ -47,11 +48,9 @@ fn HomePage() -> impl IntoView {
let (key, set_key) = create_signal("".to_string()); let (key, set_key) = create_signal("".to_string());
create_effect(move |_| { // window() doesn't work on the server so we provide use_window()
// window() doesn't work on the server let _ = use_event_listener(use_window(), keypress, move |evt: KeyboardEvent| {
let _ = use_event_listener(window(), keypress, move |evt: KeyboardEvent| { set_key(evt.key())
set_key(evt.key())
});
}); });
let (debounce_value, set_debounce_value) = create_signal("not called"); let (debounce_value, set_debounce_value) = create_signal("not called");
@ -65,10 +64,10 @@ fn HomePage() -> impl IntoView {
debounced_fn(); debounced_fn();
view! { view! {
<h1>"Leptos-Use SSR Example"</h1> <h1>Leptos-Use SSR Example</h1>
<button on:click=on_click>"Click Me: " {count}</button> <button on:click=on_click>Click Me: {count}</button>
<p>"Locale "zh-Hans-CN-u-nu-hanidec": " {zh_count}</p> <p>Locale zh-Hans-CN-u-nu-hanidec: {zh_count}</p>
<p>"Press any key: " {key}</p> <p>Press any key: {key}</p>
<p>"Debounced called: " {debounce_value}</p> <p>Debounced called: {debounce_value}</p>
} }
} }

View file

@ -2,6 +2,7 @@
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
use axum::{routing::post, Router}; use axum::{routing::post, Router};
use leptos::logging::log;
use leptos::*; use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes}; use leptos_axum::{generate_route_list, LeptosRoutes};
use start_axum::app::*; use start_axum::app::*;

View file

@ -1,5 +1,6 @@
use leptos::ev::{click, keydown}; use leptos::ev::{click, keydown};
use leptos::html::A; use leptos::html::A;
use leptos::logging::log;
use leptos::*; use leptos::*;
use leptos_use::use_event_listener; use leptos_use::use_event_listener;

View file

@ -0,0 +1,16 @@
[package]
name = "use_geolocation"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { version = "0.5.0-rc1", features = ["nightly", "csr"] }
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 `use_geolocation`.
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,47 @@
use leptos::*;
use leptos_use::docs::demo_or_body;
use leptos_use::{use_geolocation, UseGeolocationReturn};
#[component]
fn Demo() -> impl IntoView {
let UseGeolocationReturn {
coords,
located_at,
error,
resume,
pause,
} = use_geolocation();
view! {
<pre lang="json">
coords: {move || if let Some(coords) = coords() {
format!(r#"{{
accuracy: {},
latitude: {},
longitude: {},
altitude: {:?},
altitude_accuracy: {:?},
heading: {:?},
speed: {:?},
}}"#, coords.accuracy(), coords.latitude(), coords.longitude(), coords.altitude(), coords.altitude_accuracy(), coords.heading(), coords.speed())
} else {
"None".to_string()
}},
located_at: {located_at},
error: {move || if let Some(error) = error() {
error.message()
} else {"None".to_string()}},
</pre>
<button on:click=move |_| pause()>"Pause watch"</button>
<button on:click=move |_| resume()>"Resume watch"</button>
}
}
fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to(demo_or_body(), || {
view! { <Demo/> }
})
}

View file

@ -0,0 +1,289 @@
[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: ;
}
.block {
display: block;
}
.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

@ -1,7 +1,7 @@
use leptos::html::Div; use leptos::html::Div;
use leptos::*; use leptos::*;
use leptos_use::docs::demo_or_body; use leptos_use::docs::demo_or_body;
use leptos_use::use_mutation_observer_with_options; use leptos_use::{use_mutation_observer_with_options, UseMutationObserverOptions};
use std::time::Duration; use std::time::Duration;
#[component] #[component]
@ -11,9 +11,6 @@ fn Demo() -> impl IntoView {
let (class_name, set_class_name) = create_signal(String::new()); let (class_name, set_class_name) = create_signal(String::new());
let (style, set_style) = create_signal(String::new()); let (style, set_style) = create_signal(String::new());
let mut init = web_sys::MutationObserverInit::new();
init.attributes(true);
use_mutation_observer_with_options( use_mutation_observer_with_options(
el, el,
move |mutations, _| { move |mutations, _| {
@ -23,7 +20,7 @@ fn Demo() -> impl IntoView {
}); });
} }
}, },
init, UseMutationObserverOptions::default().attributes(true),
); );
let _ = set_timeout_with_handle( let _ = set_timeout_with_handle(

View file

@ -1,4 +1,5 @@
use leptos::html::Div; use leptos::html::Div;
use leptos::logging::log;
use leptos::*; use leptos::*;
use leptos_use::docs::{demo_or_body, BooleanDisplay}; use leptos_use::docs::{demo_or_body, BooleanDisplay};
use leptos_use::{use_scroll_with_options, ScrollBehavior, UseScrollOptions, UseScrollReturn}; use leptos_use::{use_scroll_with_options, ScrollBehavior, UseScrollOptions, UseScrollReturn};

View file

@ -1,3 +1,4 @@
use crate::{UseDocument, UseWindow};
use leptos::html::ElementDescriptor; use leptos::html::ElementDescriptor;
use leptos::*; use leptos::*;
use std::marker::PhantomData; use std::marker::PhantomData;
@ -153,6 +154,22 @@ where
} }
} }
macro_rules! impl_from_deref_option {
($ty:ty, $ty2:ty) => {
impl<E> From<$ty> for ElementMaybeSignal<$ty2, E>
where
E: From<$ty2> + 'static,
{
fn from(value: $ty) -> Self {
Self::Static((*value).clone())
}
}
};
}
impl_from_deref_option!(UseWindow, web_sys::Window);
impl_from_deref_option!(UseDocument, web_sys::Document);
// From string (selector) /////////////////////////////////////////////////////////////// // From string (selector) ///////////////////////////////////////////////////////////////
impl<'a, E> From<&'a str> for ElementMaybeSignal<web_sys::Element, E> impl<'a, E> From<&'a str> for ElementMaybeSignal<web_sys::Element, E>

View file

@ -1,4 +1,5 @@
use crate::core::ElementMaybeSignal; use crate::core::ElementMaybeSignal;
use crate::{UseDocument, UseWindow};
use leptos::html::ElementDescriptor; use leptos::html::ElementDescriptor;
use leptos::*; use leptos::*;
use std::marker::PhantomData; use std::marker::PhantomData;
@ -154,6 +155,22 @@ where
} }
} }
macro_rules! impl_from_deref_option {
($ty:ty, $ty2:ty) => {
impl<E> From<$ty> for ElementsMaybeSignal<$ty2, E>
where
E: From<$ty2> + 'static,
{
fn from(value: $ty) -> Self {
Self::Static(vec![(*value).clone()])
}
}
};
}
impl_from_deref_option!(UseWindow, web_sys::Window);
impl_from_deref_option!(UseDocument, web_sys::Document);
// From string (selector) /////////////////////////////////////////////////////////////// // From string (selector) ///////////////////////////////////////////////////////////////
impl<'a, E> From<&'a str> for ElementsMaybeSignal<web_sys::Node, E> impl<'a, E> From<&'a str> for ElementsMaybeSignal<web_sys::Node, E>

View file

@ -27,6 +27,9 @@ mod is_none;
mod is_ok; mod is_ok;
mod is_some; mod is_some;
mod on_click_outside; mod on_click_outside;
mod use_document;
mod use_window;
mod use_geolocation;
mod signal_debounced; mod signal_debounced;
mod signal_throttled; mod signal_throttled;
mod use_active_element; mod use_active_element;
@ -70,6 +73,9 @@ pub use is_none::*;
pub use is_ok::*; pub use is_ok::*;
pub use is_some::*; pub use is_some::*;
pub use on_click_outside::*; pub use on_click_outside::*;
pub use use_document::*;
pub use use_window::*;
pub use use_geolocation::*;
pub use signal_debounced::*; pub use signal_debounced::*;
pub use signal_throttled::*; pub use signal_throttled::*;
pub use use_active_element::*; pub use use_active_element::*;

View file

@ -1,17 +1,20 @@
use crate::core::{ElementMaybeSignal, ElementsMaybeSignal}; use crate::core::{ElementMaybeSignal, ElementsMaybeSignal};
use crate::utils::IS_IOS; use cfg_if::cfg_if;
use crate::{use_event_listener, use_event_listener_with_options};
use default_struct_builder::DefaultBuilder; 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); cfg_if! { if #[cfg(not(feature = "ssr"))] {
use leptos::*;
use crate::utils::IS_IOS;
use crate::{use_event_listener, use_event_listener_with_options, UseEventListenerOptions};
use leptos::ev::{blur, click, pointerdown};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::RwLock;
use std::time::Duration;
use wasm_bindgen::JsCast;
static IOS_WORKAROUND: RwLock<bool> = RwLock::new(false);
}}
/// Listen for clicks outside of an element. /// Listen for clicks outside of an element.
/// Useful for modals or dropdowns. /// Useful for modals or dropdowns.
@ -25,6 +28,7 @@ static IOS_WORKAROUND: RwLock<bool> = RwLock::new(false);
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::ev::resize; /// # use leptos::ev::resize;
/// # use leptos::logging::log;
/// # use leptos::html::Div; /// # use leptos::html::Div;
/// # use leptos_use::on_click_outside; /// # use leptos_use::on_click_outside;
/// # /// #
@ -48,7 +52,7 @@ static IOS_WORKAROUND: RwLock<bool> = RwLock::new(false);
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) /// On the server this amounts to a no-op.
pub fn on_click_outside<El, T, F>(target: El, handler: F) -> impl FnOnce() + Clone pub fn on_click_outside<El, T, F>(target: El, handler: F) -> impl FnOnce() + Clone
where where
El: Clone, El: Clone,
@ -64,6 +68,7 @@ where
} }
/// Version of `on_click_outside` that takes an `OnClickOutsideOptions`. See `on_click_outside` for more details. /// Version of `on_click_outside` that takes an `OnClickOutsideOptions`. See `on_click_outside` for more details.
#[cfg_attr(feature = "ssr", allow(unused_variables))]
pub fn on_click_outside_with_options<El, T, F, I>( pub fn on_click_outside_with_options<El, T, F, I>(
target: El, target: El,
handler: F, handler: F,
@ -76,151 +81,151 @@ where
F: FnMut(web_sys::Event) + Clone + 'static, F: FnMut(web_sys::Event) + Clone + 'static,
I: Into<web_sys::EventTarget> + Clone + 'static, I: Into<web_sys::EventTarget> + Clone + 'static,
{ {
let OnClickOutsideOptions { cfg_if! { if #[cfg(feature = "ssr")] {
ignore, || {}
capture, } else {
detect_iframes, let OnClickOutsideOptions {
} = options; ignore,
capture,
detect_iframes,
} = options;
// Fixes: https://github.com/vueuse/vueuse/issues/1520 // Fixes: https://github.com/vueuse/vueuse/issues/1520
// How it works: https://stackoverflow.com/a/39712411 // How it works: https://stackoverflow.com/a/39712411
if *IS_IOS { if *IS_IOS {
if let Ok(mut ios_workaround) = IOS_WORKAROUND.write() { if let Ok(mut ios_workaround) = IOS_WORKAROUND.write() {
if !*ios_workaround { if !*ios_workaround {
*ios_workaround = true; *ios_workaround = true;
if let Some(body) = document().body() { if let Some(body) = document().body() {
let children = body.children(); let children = body.children();
for i in 0..children.length() { for i in 0..children.length() {
let _ = children let _ = children
.get_with_index(i) .get_with_index(i)
.expect("checked index") .expect("checked index")
.add_event_listener_with_callback( .add_event_listener_with_callback(
"click", "click",
&js_sys::Function::default(), &js_sys::Function::default(),
); );
}
} }
} }
} }
} }
}
let should_listen = Rc::new(Cell::new(true)); let should_listen = Rc::new(Cell::new(true));
let should_ignore = move |event: &web_sys::UiEvent| { let should_ignore = move |event: &web_sys::UiEvent| {
let ignore = ignore.get_untracked(); let ignore = ignore.get_untracked();
ignore.into_iter().flatten().any(|element| { ignore.into_iter().flatten().any(|element| {
let element: web_sys::EventTarget = element.into(); let element: web_sys::EventTarget = element.into();
event_target::<web_sys::EventTarget>(event) == element event_target::<web_sys::EventTarget>(event) == element
|| event.composed_path().includes(element.as_ref(), 0) || event.composed_path().includes(element.as_ref(), 0)
}) })
}; };
let target = (target).into(); let target = (target).into();
let listener = { let listener = {
let should_listen = Rc::clone(&should_listen); let should_listen = Rc::clone(&should_listen);
let mut handler = handler.clone(); let mut handler = handler.clone();
let target = target.clone(); let target = target.clone();
let should_ignore = should_ignore.clone(); let should_ignore = should_ignore.clone();
move |event: web_sys::UiEvent| { 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, _, _>(
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, _, _>(
window(),
pointerdown,
move |event| {
if let Some(el) = target.get_untracked() { if let Some(el) = target.get_untracked() {
should_listen.set( let el = el.into();
!event.composed_path().includes(el.into().as_ref(), 0)
&& !should_ignore(&event), 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());
} }
}, }
options, };
)
};
let remove_blur_listener = if detect_iframes { let remove_click_listener = {
Some(use_event_listener::<_, web_sys::Window, _, _>( let mut listener = listener.clone();
window(),
blur,
move |event| {
let target = target.clone();
let mut handler = handler.clone();
let _ = set_timeout_with_handle( use_event_listener_with_options::<_, web_sys::Window, _, _>(
move || { window(),
if let Some(el) = target.get_untracked() { click,
if let Some(active_element) = document().active_element() { move |event| listener(event.into()),
if active_element.tag_name() == "IFRAME" UseEventListenerOptions::default()
&& !el .passive(true)
.into() .capture(capture),
.unchecked_into::<web_sys::Node>() )
.contains(Some(&active_element.into())) };
{
handler(event.into()); let remove_pointer_listener = {
let target = target.clone();
let should_listen = Rc::clone(&should_listen);
use_event_listener_with_options::<_, web_sys::Window, _, _>(
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),
);
}
},
UseEventListenerOptions::default().passive(true),
)
};
let remove_blur_listener = if detect_iframes {
Some(use_event_listener::<_, web_sys::Window, _, _>(
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,
Duration::ZERO, );
); },
}, ))
)) } else {
} else { None
None };
};
move || { move || {
remove_click_listener(); remove_click_listener();
remove_pointer_listener(); remove_pointer_listener();
if let Some(f) = remove_blur_listener { if let Some(f) = remove_blur_listener {
f(); f();
}
} }
} }}
} }
/// Options for [`on_click_outside_with_options`]. /// Options for [`on_click_outside_with_options`].
@ -230,6 +235,7 @@ where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<web_sys::EventTarget> + Clone + 'static,
{ {
/// List of elementss that should not trigger the callback. Defaults to `[]`. /// List of elementss that should not trigger the callback. Defaults to `[]`.
#[cfg_attr(feature = "ssr", allow(dead_code))]
ignore: ElementsMaybeSignal<T, web_sys::EventTarget>, ignore: ElementsMaybeSignal<T, web_sys::EventTarget>,
/// Use capturing phase for internal event listener. Defaults to `true`. /// Use capturing phase for internal event listener. Defaults to `true`.

View file

@ -1,11 +1,9 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::use_event_listener_with_options; use crate::{use_document, use_event_listener_with_options, UseEventListenerOptions};
use cfg_if::cfg_if;
use leptos::ev::{blur, focus}; use leptos::ev::{blur, focus};
use leptos::html::{AnyElement, ToHtmlElement}; use leptos::html::{AnyElement, ToHtmlElement};
use leptos::*; use leptos::*;
use web_sys::AddEventListenerOptions;
/// Reactive `document.activeElement` /// Reactive `document.activeElement`
/// ///
@ -17,6 +15,7 @@ use web_sys::AddEventListenerOptions;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// use leptos_use::use_active_element; /// use leptos_use::use_active_element;
/// # /// #
/// # #[component] /// # #[component]
@ -35,44 +34,37 @@ use web_sys::AddEventListenerOptions;
/// ///
/// On the server this returns a `Signal` that always contains the value `None`. /// On the server this returns a `Signal` that always contains the value `None`.
pub fn use_active_element() -> Signal<Option<HtmlElement<AnyElement>>> { pub fn use_active_element() -> Signal<Option<HtmlElement<AnyElement>>> {
cfg_if! { if #[cfg(feature = "ssr")] { let get_active_element = move || {
let get_active_element = || { None }; use_document()
} else { .active_element()
let get_active_element = move || { .map(|el| el.to_leptos_element())
document() };
.active_element()
.map(|el| el.to_leptos_element())
};
}}
let (active_element, set_active_element) = create_signal(get_active_element()); let (active_element, set_active_element) = create_signal(get_active_element());
cfg_if! { if #[cfg(not(feature = "ssr"))] { let listener_options = UseEventListenerOptions::default().capture(true);
let mut listener_options = AddEventListenerOptions::new();
listener_options.capture(true);
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
window(), window(),
blur, blur,
move |event| { move |event| {
if event.related_target().is_some() { if event.related_target().is_some() {
return; return;
} }
set_active_element.update(|el| *el = get_active_element()); set_active_element.update(|el| *el = get_active_element());
}, },
listener_options.clone(), listener_options,
); );
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
window(), window(),
focus, focus,
move |_| { move |_| {
set_active_element.update(|el| *el = get_active_element()); set_active_element.update(|el| *el = get_active_element());
}, },
listener_options, listener_options,
); );
}}
active_element.into() active_element.into()
} }

View file

@ -1,4 +1,5 @@
use crate::use_media_query; use crate::use_media_query;
use leptos::logging::error;
use leptos::*; use leptos::*;
use paste::paste; use paste::paste;
use std::collections::HashMap; use std::collections::HashMap;

View file

@ -1,13 +1,16 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::core::ElementMaybeSignal; use crate::core::ElementMaybeSignal;
use crate::{use_mutation_observer_with_options, watch_with_options, WatchOptions}; use crate::{
use_mutation_observer_with_options, watch_with_options, UseMutationObserverOptions,
WatchOptions,
};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::time::Duration; use std::time::Duration;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::JsCast;
/// Manipulate CSS variables. /// Manipulate CSS variables.
/// ///
@ -127,17 +130,14 @@ where
}; };
if observe { if observe {
let mut init = web_sys::MutationObserverInit::new();
let update_css_var = update_css_var.clone(); let update_css_var = update_css_var.clone();
let el_signal = el_signal.clone(); let el_signal = el_signal.clone();
init.attribute_filter(&js_sys::Array::from_iter(
vec![JsValue::from_str("style")],
));
use_mutation_observer_with_options::<ElementMaybeSignal<T, web_sys::Element>, T, _>( use_mutation_observer_with_options::<ElementMaybeSignal<T, web_sys::Element>, T, _>(
el_signal, el_signal,
move |_, _| update_css_var(), move |_, _| update_css_var(),
init, UseMutationObserverOptions::default()
.attribute_filter(vec!["style".to_string()]),
); );
} }

View file

@ -12,6 +12,7 @@ use leptos::*;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// use leptos_use::{use_cycle_list, UseCycleListReturn}; /// use leptos_use::{use_cycle_list, UseCycleListReturn};
/// # /// #
/// # #[component] /// # #[component]

56
src/use_document.rs Normal file
View file

@ -0,0 +1,56 @@
use cfg_if::cfg_if;
use std::ops::Deref;
#[cfg(not(feature = "ssr"))]
use leptos::*;
/// SSR safe `document()`.
/// This returns just a new-type wrapper around `Option<Document>`.
/// Calling this amounts to `None` on the server and `Some(Document)` on the client.
///
/// It provides some convenient methods for working with the document like `body()`.
///
/// ## Usage
///
/// ```
/// # use leptos::*;
/// # use leptos_use::use_document;
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// let document = use_document();
///
/// // Returns `None` on the server but will not panic.
/// let body = document.body();
/// #
/// # view! { }
/// # }
/// ```
pub fn use_document() -> UseDocument {
cfg_if! { if #[cfg(feature = "ssr")] {
UseDocument(None)
} else {
UseDocument(Some(document()))
}}
}
/// Return type of [`use_document`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UseDocument(Option<web_sys::Document>);
impl Deref for UseDocument {
type Target = Option<web_sys::Document>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl UseDocument {
pub fn body(&self) -> Option<web_sys::HtmlElement> {
self.0.as_ref().and_then(|d| d.body())
}
pub fn active_element(&self) -> Option<web_sys::Element> {
self.0.as_ref().and_then(|d| d.active_element())
}
}

View file

@ -1,5 +1,5 @@
use crate::core::{ElementMaybeSignal, MaybeRwSignal, PointerType, Position}; use crate::core::{ElementMaybeSignal, MaybeRwSignal, PointerType, Position};
use crate::use_event_listener_with_options; use crate::{use_event_listener_with_options, UseEventListenerOptions};
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::ev::{pointerdown, pointermove, pointerup}; use leptos::ev::{pointerdown, pointermove, pointerup};
use leptos::*; use leptos::*;
@ -183,20 +183,19 @@ where
handle_event(event); handle_event(event);
}; };
let mut listener_options = web_sys::AddEventListenerOptions::new(); let listener_options = UseEventListenerOptions::default().capture(true);
listener_options.capture(true);
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
dragging_handle, dragging_handle,
pointerdown, pointerdown,
on_pointer_down, on_pointer_down,
listener_options.clone(), listener_options,
); );
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
dragging_element.clone(), dragging_element.clone(),
pointermove, pointermove,
on_pointer_move, on_pointer_move,
listener_options.clone(), listener_options,
); );
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
dragging_element, dragging_element,

View file

@ -1,11 +1,15 @@
use crate::core::ElementMaybeSignal; use crate::core::ElementMaybeSignal;
use crate::use_event_listener; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::ev::{dragenter, dragleave, dragover, drop};
use leptos::*; use leptos::*;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::rc::Rc; use std::rc::Rc;
cfg_if! { if #[cfg(not(feature = "ssr"))] {
use crate::use_event_listener;
use leptos::ev::{dragenter, dragleave, dragover, drop};
}}
/// Create a zone where files can be dropped. /// Create a zone where files can be dropped.
/// ///
/// ## Demo /// ## Demo
@ -45,7 +49,8 @@ use std::rc::Rc;
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) /// On the server the returned `file` signal always contains an empty `Vec` and
/// `is_over_drop_zone` contains always `false`
pub fn use_drop_zone<El, T>(target: El) -> UseDropZoneReturn pub fn use_drop_zone<El, T>(target: El) -> UseDropZoneReturn
where where
El: Clone, El: Clone,
@ -56,6 +61,7 @@ where
} }
/// Version of [`use_drop_zone`] that takes a `UseDropZoneOptions`. See [`use_drop_zone`] for how to use. /// Version of [`use_drop_zone`] that takes a `UseDropZoneOptions`. See [`use_drop_zone`] for how to use.
#[cfg_attr(feature = "ssr", allow(unused_variables))]
pub fn use_drop_zone_with_options<El, T>( pub fn use_drop_zone_with_options<El, T>(
target: El, target: El,
options: UseDropZoneOptions, options: UseDropZoneOptions,
@ -65,81 +71,83 @@ where
El: Into<ElementMaybeSignal<T, web_sys::EventTarget>>, El: Into<ElementMaybeSignal<T, web_sys::EventTarget>>,
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<web_sys::EventTarget> + Clone + 'static,
{ {
let UseDropZoneOptions {
on_drop,
on_enter,
on_leave,
on_over,
} = options;
let (is_over_drop_zone, set_over_drop_zone) = create_signal(false); let (is_over_drop_zone, set_over_drop_zone) = create_signal(false);
let (files, set_files) = create_signal(Vec::<web_sys::File>::new()); let (files, set_files) = create_signal(Vec::<web_sys::File>::new());
let counter = store_value(0_usize); cfg_if! { if #[cfg(not(feature = "ssr"))] {
let UseDropZoneOptions {
on_drop,
on_enter,
on_leave,
on_over,
} = options;
let update_files = move |event: &web_sys::DragEvent| { let counter = store_value(0_usize);
if let Some(data_transfer) = event.data_transfer() {
let files: Vec<web_sys::File> = data_transfer
.files()
.map(|f| js_sys::Array::from(&f).to_vec())
.unwrap_or_default()
.into_iter()
.map(web_sys::File::from)
.collect();
set_files.update(move |f| *f = files); let update_files = move |event: &web_sys::DragEvent| {
} if let Some(data_transfer) = event.data_transfer() {
}; let files: Vec<web_sys::File> = data_transfer
.files()
.map(|f| js_sys::Array::from(&f).to_vec())
.unwrap_or_default()
.into_iter()
.map(web_sys::File::from)
.collect();
let _ = use_event_listener(target.clone(), dragenter, move |event| { set_files.update(move |f| *f = files);
event.prevent_default(); }
counter.update_value(|counter| *counter += 1); };
set_over_drop_zone.set(true);
update_files(&event); let _ = use_event_listener(target.clone(), dragenter, move |event| {
event.prevent_default();
counter.update_value(|counter| *counter += 1);
set_over_drop_zone.set(true);
on_enter(UseDropZoneEvent { update_files(&event);
files: files.get_untracked(),
event, on_enter(UseDropZoneEvent {
files: files.get_untracked(),
event,
});
}); });
});
let _ = use_event_listener(target.clone(), dragover, move |event| { let _ = use_event_listener(target.clone(), dragover, move |event| {
event.prevent_default(); event.prevent_default();
update_files(&event); update_files(&event);
on_over(UseDropZoneEvent { on_over(UseDropZoneEvent {
files: files.get_untracked(), files: files.get_untracked(),
event, event,
});
}); });
});
let _ = use_event_listener(target.clone(), dragleave, move |event| { let _ = use_event_listener(target.clone(), dragleave, move |event| {
event.prevent_default(); event.prevent_default();
counter.update_value(|counter| *counter -= 1); counter.update_value(|counter| *counter -= 1);
if counter.get_value() == 0 { if counter.get_value() == 0 {
set_over_drop_zone.set(false);
}
update_files(&event);
on_leave(UseDropZoneEvent {
files: files.get_untracked(),
event,
});
});
let _ = use_event_listener(target, drop, move |event| {
event.prevent_default();
counter.update_value(|counter| *counter = 0);
set_over_drop_zone.set(false); set_over_drop_zone.set(false);
}
update_files(&event); update_files(&event);
on_leave(UseDropZoneEvent { on_drop(UseDropZoneEvent {
files: files.get_untracked(), files: files.get_untracked(),
event, event,
});
}); });
}); }}
let _ = use_event_listener(target, drop, move |event| {
event.prevent_default();
counter.update_value(|counter| *counter = 0);
set_over_drop_zone.set(false);
update_files(&event);
on_drop(UseDropZoneEvent {
files: files.get_untracked(),
event,
});
});
UseDropZoneReturn { UseDropZoneReturn {
files: files.into(), files: files.into(),
@ -149,6 +157,7 @@ where
/// Options for [`use_drop_zone_with_options`]. /// Options for [`use_drop_zone_with_options`].
#[derive(DefaultBuilder, Clone)] #[derive(DefaultBuilder, Clone)]
#[cfg_attr(feature = "ssr", allow(dead_code))]
pub struct UseDropZoneOptions { pub struct UseDropZoneOptions {
/// Event handler for the [`drop`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event) event /// Event handler for the [`drop`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event) event
on_drop: Rc<dyn Fn(UseDropZoneEvent)>, on_drop: Rc<dyn Fn(UseDropZoneEvent)>,

View file

@ -1,11 +1,14 @@
use crate::core::ElementMaybeSignal; use crate::core::ElementMaybeSignal;
use crate::use_event_listener_with_options; use crate::{use_event_listener_with_options, UseEventListenerOptions};
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::ev::{mouseenter, mouseleave}; use leptos::ev::{mouseenter, mouseleave};
use leptos::leptos_dom::helpers::TimeoutHandle; use leptos::leptos_dom::helpers::TimeoutHandle;
use leptos::*; use leptos::*;
use std::time::Duration;
use web_sys::AddEventListenerOptions; cfg_if! { if #[cfg(not(feature = "ssr"))] {
use std::time::Duration;
}}
/// Reactive element's hover state. /// Reactive element's hover state.
/// ///
@ -33,7 +36,7 @@ use web_sys::AddEventListenerOptions;
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) /// On the server this returns a `Signal` that always contains the value `false`.
pub fn use_element_hover<El, T>(el: El) -> Signal<bool> pub fn use_element_hover<El, T>(el: El) -> Signal<bool>
where where
El: Clone, El: Clone,
@ -45,6 +48,7 @@ where
/// Version of [`use_element_hover`] that takes a `UseElementHoverOptions`. See [`use_element_hover`] for how to use. /// Version of [`use_element_hover`] that takes a `UseElementHoverOptions`. See [`use_element_hover`] for how to use.
#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
pub fn use_element_hover_with_options<El, T>( pub fn use_element_hover_with_options<El, T>(
el: El, el: El,
options: UseElementHoverOptions, options: UseElementHoverOptions,
@ -64,31 +68,32 @@ where
let mut timer: Option<TimeoutHandle> = None; let mut timer: Option<TimeoutHandle> = None;
let mut toggle = move |entering: bool| { let mut toggle = move |entering: bool| {
let delay = if entering { delay_enter } else { delay_leave }; cfg_if! { if #[cfg(not(feature = "ssr"))] {
let delay = if entering { delay_enter } else { delay_leave };
if let Some(handle) = timer.take() { if let Some(handle) = timer.take() {
handle.clear(); handle.clear();
} }
if delay > 0 { if delay > 0 {
timer = set_timeout_with_handle( timer = set_timeout_with_handle(
move || set_hovered.set(entering), move || set_hovered.set(entering),
Duration::from_millis(delay), Duration::from_millis(delay),
) )
.ok(); .ok();
} else { } else {
set_hovered.set(entering); set_hovered.set(entering);
} }
}}
}; };
let mut listener_options = AddEventListenerOptions::new(); let listener_options = UseEventListenerOptions::default().passive(true);
listener_options.passive(true);
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
el.clone(), el.clone(),
mouseenter, mouseenter,
move |_| toggle(true), move |_| toggle(true),
listener_options.clone(), listener_options,
); );
let _ = let _ =

View file

@ -1,10 +1,14 @@
use crate::core::{ElementMaybeSignal, Size}; use crate::core::{ElementMaybeSignal, Size};
use crate::{use_resize_observer_with_options, UseResizeObserverOptions}; use cfg_if::cfg_if;
use crate::{watch_with_options, WatchOptions};
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsCast;
cfg_if! { if #[cfg(not(feature = "ssr"))] {
use crate::{use_resize_observer_with_options, UseResizeObserverOptions};
use crate::{watch_with_options, WatchOptions};
use wasm_bindgen::JsCast;
}}
/// Reactive size of an HTML element. /// Reactive size of an HTML element.
/// ///
@ -41,7 +45,7 @@ use wasm_bindgen::JsCast;
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) /// On the server the returned signals always contain the value of the `initial_size` option.
/// ///
/// ## See also /// ## See also
/// ///
@ -56,6 +60,7 @@ where
} }
/// Version of [`use_element_size`] that takes a `UseElementSizeOptions`. See [`use_element_size`] for how to use. /// Version of [`use_element_size`] that takes a `UseElementSizeOptions`. See [`use_element_size`] for how to use.
#[cfg_attr(feature = "ssr", allow(unused_variables))]
pub fn use_element_size_with_options<El, T>( pub fn use_element_size_with_options<El, T>(
target: El, target: El,
options: UseElementSizeOptions, options: UseElementSizeOptions,
@ -65,101 +70,104 @@ where
El: Into<ElementMaybeSignal<T, web_sys::Element>>, El: Into<ElementMaybeSignal<T, web_sys::Element>>,
T: Into<web_sys::Element> + Clone + 'static, T: Into<web_sys::Element> + Clone + 'static,
{ {
let window = window(); let UseElementSizeOptions { box_, initial_size } = options;
let box_ = options.box_;
let initial_size = options.initial_size;
let target = (target).into(); let (width, set_width) = create_signal(initial_size.width);
let (height, set_height) = create_signal(initial_size.height);
let is_svg = { cfg_if! { if #[cfg(not(feature = "ssr"))] {
let target = target.clone();
move || { let box_ = box_.unwrap_or(web_sys::ResizeObserverBoxOptions::ContentBox);
if let Some(target) = target.get_untracked() {
target let target = target.into();
.into()
.namespace_uri() let is_svg = {
.map(|ns| ns.contains("svg")) let target = target.clone();
.unwrap_or(false)
} else { move || {
false 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(options.initial_size.width); {
let (height, set_height) = create_signal(options.initial_size.height); let target = target.clone();
{ let _ = use_resize_observer_with_options::<ElementMaybeSignal<T, web_sys::Element>, _, _>(
let target = target.clone(); target.clone(),
move |entries, _| {
let entry = &entries[0];
let _ = use_resize_observer_with_options::<ElementMaybeSignal<T, web_sys::Element>, _, _>( let box_size = match box_ {
target.clone(), web_sys::ResizeObserverBoxOptions::ContentBox => entry.content_box_size(),
move |entries, _| { web_sys::ResizeObserverBoxOptions::BorderBox => entry.border_box_size(),
let entry = &entries[0]; web_sys::ResizeObserverBoxOptions::DevicePixelContentBox => {
entry.device_pixel_content_box_size()
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!(),
};
if is_svg() {
if let Some(target) = target.get() {
if let Ok(Some(styles)) = window.get_computed_style(&target.into()) {
set_height.set(
styles
.get_property_value("height")
.map(|v| v.parse().unwrap_or_default())
.unwrap_or_default(),
);
set_width.set(
styles
.get_property_value("width")
.map(|v| v.parse().unwrap_or_default())
.unwrap_or_default(),
);
} }
} _ => unreachable!(),
} 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.set(format_box_size.iter().fold(0.0, |acc, v| { if is_svg() {
acc + v.as_ref().clone().unchecked_into::<BoxSize>().inline_size() if let Some(target) = target.get() {
})); if let Ok(Some(styles)) = window().get_computed_style(&target.into()) {
set_height.set(format_box_size.iter().fold(0.0, |acc, v| { set_height.set(
acc + v.as_ref().clone().unchecked_into::<BoxSize>().block_size() styles
})) .get_property_value("height")
.map(|v| v.parse().unwrap_or_default())
.unwrap_or_default(),
);
set_width.set(
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.set(format_box_size.iter().fold(0.0, |acc, v| {
acc + v.as_ref().clone().unchecked_into::<BoxSize>().inline_size()
}));
set_height.set(format_box_size.iter().fold(0.0, |acc, v| {
acc + v.as_ref().clone().unchecked_into::<BoxSize>().block_size()
}))
} else {
// fallback
set_width.set(entry.content_rect().width());
set_height.set(entry.content_rect().height())
}
},
UseResizeObserverOptions::default().box_(box_),
);
}
let _ = watch_with_options(
move || target.get(),
move |ele, _, _| {
if ele.is_some() {
set_width.set(initial_size.width);
set_height.set(initial_size.height);
} else { } else {
// fallback set_width.set(0.0);
set_width.set(entry.content_rect().width()); set_height.set(0.0);
set_height.set(entry.content_rect().height())
} }
}, },
options.into(), WatchOptions::default().immediate(false),
); );
} }}
let _ = watch_with_options(
move || target.get(),
move |ele, _, _| {
if ele.is_some() {
set_width.set(initial_size.width);
set_height.set(initial_size.height);
} else {
set_width.set(0.0);
set_height.set(0.0);
}
},
WatchOptions::default().immediate(false),
);
UseElementSizeReturn { UseElementSizeReturn {
width: width.into(), width: width.into(),
@ -175,24 +183,19 @@ pub struct UseElementSizeOptions {
initial_size: Size, initial_size: Size,
/// The box that is used to determine the dimensions of the target. Defaults to `ContentBox`. /// The box that is used to determine the dimensions of the target. Defaults to `ContentBox`.
pub box_: web_sys::ResizeObserverBoxOptions, #[builder(into)]
pub box_: Option<web_sys::ResizeObserverBoxOptions>,
} }
impl Default for UseElementSizeOptions { impl Default for UseElementSizeOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
initial_size: Size::default(), initial_size: Size::default(),
box_: web_sys::ResizeObserverBoxOptions::ContentBox, box_: None,
} }
} }
} }
impl From<UseElementSizeOptions> for UseResizeObserverOptions {
fn from(options: UseElementSizeOptions) -> Self {
Self::default().box_(options.box_)
}
}
/// The return value of [`use_element_size`]. /// The return value of [`use_element_size`].
pub struct UseElementSizeReturn { pub struct UseElementSizeReturn {
/// The width of the element. /// The width of the element.

View file

@ -1,9 +1,12 @@
use crate::core::ElementMaybeSignal; use crate::core::ElementMaybeSignal;
use crate::{use_intersection_observer_with_options, UseIntersectionObserverOptions}; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use std::marker::PhantomData; use std::marker::PhantomData;
#[cfg(not(feature = "ssr"))]
use crate::{use_intersection_observer_with_options, UseIntersectionObserverOptions};
/// Tracks the visibility of an element within the viewport. /// Tracks the visibility of an element within the viewport.
/// ///
/// ## Demo /// ## Demo
@ -33,7 +36,7 @@ use std::marker::PhantomData;
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) /// On the server this returns a `Signal` that always contains the value `false`.
/// ///
/// ## See also /// ## See also
/// ///
@ -49,6 +52,8 @@ where
) )
} }
/// Version of [`use_element_visibility`] with that takes a `UseElementVisibilityOptions`. See [`use_element_visibility`] for how to use.
#[cfg_attr(feature = "ssr", allow(unused_variables))]
pub fn use_element_visibility_with_options<El, T, ContainerEl, ContainerT>( pub fn use_element_visibility_with_options<El, T, ContainerEl, ContainerT>(
target: El, target: El,
options: UseElementVisibilityOptions<ContainerEl, ContainerT>, options: UseElementVisibilityOptions<ContainerEl, ContainerT>,
@ -61,20 +66,22 @@ where
{ {
let (is_visible, set_visible) = create_signal(false); let (is_visible, set_visible) = create_signal(false);
use_intersection_observer_with_options( cfg_if! { if #[cfg(not(feature = "ssr"))] {
target.into(), use_intersection_observer_with_options(
move |entries, _| { target.into(),
// In some circumstances Chrome passes a first (or only) entry which has a zero bounding client rect move |entries, _| {
// and returns `is_intersecting` erroneously as `false`. // In some circumstances Chrome passes a first (or only) entry which has a zero bounding client rect
if let Some(entry) = entries.into_iter().find(|entry| { // and returns `is_intersecting` erroneously as `false`.
let rect = entry.bounding_client_rect(); if let Some(entry) = entries.into_iter().find(|entry| {
rect.width() > 0.0 || rect.height() > 0.0 let rect = entry.bounding_client_rect();
}) { rect.width() > 0.0 || rect.height() > 0.0
set_visible.set(entry.is_intersecting()); }) {
} set_visible.set(entry.is_intersecting());
}, }
UseIntersectionObserverOptions::default().root(options.viewport), },
); UseIntersectionObserverOptions::default().root(options.viewport),
);
}}
is_visible.into() is_visible.into()
} }
@ -92,6 +99,7 @@ where
/// Defaults to `None` (which means the root `document` will be used). /// Defaults to `None` (which means the root `document` will be used).
/// Please note that setting this to a `Some(document)` may not be supported by all browsers. /// Please note that setting this to a `Some(document)` may not be supported by all browsers.
/// See [Browser Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#browser_compatibility) /// See [Browser Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#browser_compatibility)
#[cfg_attr(feature = "ssr", allow(dead_code))]
viewport: Option<El>, viewport: Option<El>,
#[builder(skip)] #[builder(skip)]

View file

@ -1,11 +1,16 @@
use crate::core::ElementMaybeSignal; use crate::core::ElementMaybeSignal;
use crate::{watch_with_options, WatchOptions}; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder;
use leptos::ev::EventDescriptor; use leptos::ev::EventDescriptor;
use leptos::*;
use std::cell::RefCell; cfg_if! { if #[cfg(not(feature = "ssr"))] {
use std::rc::Rc; use crate::{watch_with_options, WatchOptions};
use wasm_bindgen::closure::Closure; use leptos::*;
use wasm_bindgen::JsCast; use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
}}
/// Use EventListener with ease. /// Use EventListener with ease.
/// Register using [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) on mounted, /// Register using [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) on mounted,
@ -16,11 +21,12 @@ use wasm_bindgen::JsCast;
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::ev::visibilitychange; /// # use leptos::ev::visibilitychange;
/// # use leptos_use::use_event_listener; /// # use leptos::logging::log;
/// # use leptos_use::{use_document, use_event_listener};
/// # /// #
/// # #[component] /// # #[component]
/// # fn Demo() -> impl IntoView { /// # fn Demo() -> impl IntoView {
/// use_event_listener(document(), visibilitychange, |evt| { /// use_event_listener(use_document(), visibilitychange, |evt| {
/// log!("{:?}", evt); /// log!("{:?}", evt);
/// }); /// });
/// # view! { } /// # view! { }
@ -33,6 +39,7 @@ use wasm_bindgen::JsCast;
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::ev::click; /// # use leptos::ev::click;
/// # use leptos::logging::log;
/// # use leptos_use::use_event_listener; /// # use leptos_use::use_event_listener;
/// # /// #
/// # #[component] /// # #[component]
@ -61,6 +68,7 @@ use wasm_bindgen::JsCast;
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::ev::keydown; /// # use leptos::ev::keydown;
/// # use leptos::logging::log;
/// # use web_sys::KeyboardEvent; /// # use web_sys::KeyboardEvent;
/// # use leptos_use::use_event_listener; /// # use leptos_use::use_event_listener;
/// # /// #
@ -78,7 +86,7 @@ use wasm_bindgen::JsCast;
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) /// On the server this amounts to a noop.
pub fn use_event_listener<Ev, El, T, F>(target: El, event: Ev, handler: F) -> impl Fn() + Clone pub fn use_event_listener<Ev, El, T, F>(target: El, event: Ev, handler: F) -> impl Fn() + Clone
where where
Ev: EventDescriptor + 'static, Ev: EventDescriptor + 'static,
@ -86,20 +94,16 @@ where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<web_sys::EventTarget> + Clone + 'static,
F: FnMut(<Ev as EventDescriptor>::EventType) + 'static, F: FnMut(<Ev as EventDescriptor>::EventType) + 'static,
{ {
use_event_listener_with_options( use_event_listener_with_options(target, event, handler, UseEventListenerOptions::default())
target,
event,
handler,
web_sys::AddEventListenerOptions::new(),
)
} }
/// Version of [`use_event_listener`] that takes `web_sys::AddEventListenerOptions`. See the docs for [`use_event_listener`] for how to use. /// Version of [`use_event_listener`] that takes `web_sys::AddEventListenerOptions`. See the docs for [`use_event_listener`] for how to use.
#[cfg_attr(feature = "ssr", allow(unused_variables))]
pub fn use_event_listener_with_options<Ev, El, T, F>( pub fn use_event_listener_with_options<Ev, El, T, F>(
target: El, target: El,
event: Ev, event: Ev,
handler: F, handler: F,
options: web_sys::AddEventListenerOptions, options: UseEventListenerOptions,
) -> impl Fn() + Clone ) -> impl Fn() + Clone
where where
Ev: EventDescriptor + 'static, Ev: EventDescriptor + 'static,
@ -107,65 +111,122 @@ where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<web_sys::EventTarget> + Clone + 'static,
F: FnMut(<Ev as EventDescriptor>::EventType) + 'static, F: FnMut(<Ev as EventDescriptor>::EventType) + 'static,
{ {
let event_name = event.name(); cfg_if! { if #[cfg(feature = "ssr")] {
let closure_js = Closure::wrap(Box::new(handler) as Box<dyn FnMut(_)>).into_js_value(); || {}
} else {
let event_name = event.name();
let closure_js = Closure::wrap(Box::new(handler) as Box<dyn FnMut(_)>).into_js_value();
let cleanup_fn = { let cleanup_fn = {
let closure_js = closure_js.clone(); let closure_js = closure_js.clone();
let options = options.clone(); let options = options.as_add_event_listener_options();
move |element: &web_sys::EventTarget| { move |element: &web_sys::EventTarget| {
let _ = element.remove_event_listener_with_callback_and_event_listener_options( let _ = element.remove_event_listener_with_callback_and_event_listener_options(
&event_name, &event_name,
closure_js.as_ref().unchecked_ref(), closure_js.as_ref().unchecked_ref(),
options.unchecked_ref(), options.unchecked_ref(),
); );
}
};
let event_name = event.name();
let signal = (target).into();
let prev_element = Rc::new(RefCell::new(None::<web_sys::EventTarget>));
let cleanup_prev_element = {
let prev_element = prev_element.clone();
move || {
if let Some(element) = prev_element.take() {
cleanup_fn(&element);
} }
} };
};
let stop_watch = { let event_name = event.name();
let cleanup_prev_element = cleanup_prev_element.clone();
watch_with_options( let signal = (target).into();
move || signal.get().map(|e| e.into()),
move |element, _, _| {
cleanup_prev_element();
prev_element.replace(element.clone());
if let Some(element) = element { let prev_element = Rc::new(RefCell::new(None::<web_sys::EventTarget>));
_ = element.add_event_listener_with_callback_and_add_event_listener_options(
&event_name, let cleanup_prev_element = {
closure_js.as_ref().unchecked_ref(), let prev_element = prev_element.clone();
&options,
); move || {
if let Some(element) = prev_element.take() {
cleanup_fn(&element);
} }
}, }
WatchOptions::default().immediate(true), };
)
};
let stop = move || { let stop_watch = {
stop_watch(); let cleanup_prev_element = cleanup_prev_element.clone();
cleanup_prev_element();
};
on_cleanup(stop.clone()); watch_with_options(
move || signal.get().map(|e| e.into()),
move |element, _, _| {
cleanup_prev_element();
prev_element.replace(element.clone());
stop if let Some(element) = element {
let options = options.as_add_event_listener_options();
_ = 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();
cleanup_prev_element();
};
on_cleanup(stop.clone());
stop
}}
}
/// Options for [`use_event_listener_with_options`].
#[derive(DefaultBuilder, Default, Copy, Clone)]
#[cfg_attr(feature = "ssr", allow(dead_code))]
pub struct UseEventListenerOptions {
/// A boolean value indicating that events of this type will be dispatched to
/// the registered `listener` before being dispatched to any `EventTarget`
/// beneath it in the DOM tree. If not specified, defaults to `false`.
capture: bool,
/// A boolean value indicating that the `listener` should be invoked at most
/// once after being added. If `true`, the `listener` would be automatically
/// removed when invoked. If not specified, defaults to `false`.
once: bool,
/// A boolean value that, if `true`, indicates that the function specified by
/// `listener` will never call
/// [`preventDefault()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault "preventDefault()").
/// If a passive listener does call `preventDefault()`, the user agent will do
/// nothing other than generate a console warning. If not specified,
/// defaults to `false` except that in browsers other than Safari,
/// defaults to `true` for the
/// [`wheel`](https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event "wheel"),
/// [`mousewheel`](https://developer.mozilla.org/en-US/docs/Web/API/Element/mousewheel_event "mousewheel"),
/// [`touchstart`](https://developer.mozilla.org/en-US/docs/Web/API/Element/touchstart_event "touchstart") and
/// [`touchmove`](https://developer.mozilla.org/en-US/docs/Web/API/Element/touchmove_event "touchmove")
/// events. See [Improving scrolling performance with passive listeners](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners)
/// to learn more.
#[builder(into)]
passive: Option<bool>,
}
impl UseEventListenerOptions {
#[cfg_attr(feature = "ssr", allow(dead_code))]
fn as_add_event_listener_options(&self) -> web_sys::AddEventListenerOptions {
let UseEventListenerOptions {
capture,
once,
passive,
} = self;
let mut options = web_sys::AddEventListenerOptions::new();
options.capture(*capture);
options.once(*once);
if let Some(passive) = passive {
options.passive(*passive);
}
options
}
} }

201
src/use_geolocation.rs Normal file
View file

@ -0,0 +1,201 @@
use crate::use_window;
use default_struct_builder::DefaultBuilder;
use leptos::*;
use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
/// Reactive [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API).
/// It allows the user to provide their location to web applications if they so desire. For privacy reasons,
/// the user is asked for permission to report location information.
///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_geolocation)
///
/// ## Usage
///
/// ```
/// # use leptos::*;
/// # use leptos_use::{use_geolocation, UseGeolocationReturn};
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// let UseGeolocationReturn {
/// coords,
/// located_at,
/// error,
/// resume,
/// pause,
/// } = use_geolocation();
/// #
/// # view! { }
/// # }
/// ```
pub fn use_geolocation() -> UseGeolocationReturn<impl Fn() + Clone, impl Fn() + Clone> {
use_geolocation_with_options(UseGeolocationOptions::default())
}
/// Version of [`use_geolocation`] that takes a `UseGeolocationOptions`. See [`use_geolocation`] for how to use.
pub fn use_geolocation_with_options(
options: UseGeolocationOptions,
) -> UseGeolocationReturn<impl Fn() + Clone, impl Fn() + Clone> {
let (located_at, set_located_at) = create_signal(None::<f64>);
let (error, set_error) = create_signal(None::<web_sys::PositionError>);
let (coords, set_coords) = create_signal(None::<web_sys::Coordinates>);
let update_position = move |position: web_sys::Position| {
set_located_at.set(Some(position.timestamp()));
set_coords.set(Some(position.coords()));
set_error.set(None);
};
let on_error = move |err: web_sys::PositionError| {
set_error.set(Some(err));
};
let watch_handle = Rc::new(Cell::new(None::<i32>));
let resume = {
let watch_handle = Rc::clone(&watch_handle);
let position_options = options.as_position_options();
move || {
let navigator = use_window().navigator();
if let Some(navigator) = navigator {
if let Ok(geolocation) = navigator.geolocation() {
let update_position =
Closure::wrap(Box::new(update_position) as Box<dyn Fn(web_sys::Position)>);
let on_error =
Closure::wrap(Box::new(on_error) as Box<dyn Fn(web_sys::PositionError)>);
watch_handle.replace(
geolocation
.watch_position_with_error_callback_and_options(
update_position.as_ref().unchecked_ref(),
Some(on_error.as_ref().unchecked_ref()),
&position_options,
)
.ok(),
);
update_position.forget();
on_error.forget();
}
}
}
};
if options.immediate {
resume();
}
let pause = {
let watch_handle = Rc::clone(&watch_handle);
move || {
let navigator = use_window().navigator();
if let Some(navigator) = navigator {
if let Some(handle) = watch_handle.take() {
if let Ok(geolocation) = navigator.geolocation() {
geolocation.clear_watch(handle);
}
}
}
}
};
on_cleanup({
let pause = pause.clone();
move || {
pause();
}
});
UseGeolocationReturn {
coords: coords.into(),
located_at: located_at.into(),
error: error.into(),
resume,
pause,
}
}
/// Options for [`use_geolocation_with_options`].
#[derive(DefaultBuilder)]
pub struct UseGeolocationOptions {
/// If `true` the geolocation watch is started when this function is called.
/// If `false` you have to call `resume` manually to start it. Defaults to `true`.
immediate: bool,
/// A boolean value that indicates the application would like to receive the best
/// possible results. If `true` and if the device is able to provide a more accurate
/// position, it will do so. Note that this can result in slower response times or
/// increased power consumption (with a GPS chip on a mobile device for example).
/// On the other hand, if `false`, the device can take the liberty to save
/// resources by responding more quickly and/or using less power. Default: `false`.
enable_high_accuracy: bool,
/// A positive value indicating the maximum age in milliseconds of a possible cached position that is acceptable to return.
/// If set to `0`, it means that the device cannot use a cached position and must attempt to retrieve the real current position.
/// Default: 30000.
maximum_age: u32,
/// A positive value representing the maximum length of time (in milliseconds)
/// the device is allowed to take in order to return a position.
/// The default value is 27000.
timeout: u32,
}
impl Default for UseGeolocationOptions {
fn default() -> Self {
Self {
enable_high_accuracy: false,
maximum_age: 30000,
timeout: 27000,
immediate: true,
}
}
}
impl UseGeolocationOptions {
fn as_position_options(&self) -> web_sys::PositionOptions {
let UseGeolocationOptions {
enable_high_accuracy,
maximum_age,
timeout,
..
} = self;
let mut options = web_sys::PositionOptions::new();
options.enable_high_accuracy(*enable_high_accuracy);
options.maximum_age(*maximum_age);
options.timeout(*timeout);
options
}
}
/// Return type of [`use_geolocation`].
pub struct UseGeolocationReturn<ResumeFn, PauseFn>
where
ResumeFn: Fn() + Clone,
PauseFn: Fn() + Clone,
{
/// The coordinates of the current device like latitude and longitude.
/// See [`GeolocationCoordinates`](https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates)..
pub coords: Signal<Option<web_sys::Coordinates>>,
/// The timestamp of the current coordinates.
pub located_at: Signal<Option<f64>>,
/// The last error received from `navigator.geolocation`.
pub error: Signal<Option<web_sys::PositionError>>,
/// Resume the geolocation watch.
pub resume: ResumeFn,
/// Pause the geolocation watch.
pub pause: PauseFn,
}

View file

@ -1,11 +1,15 @@
use crate::core::{ElementMaybeSignal, ElementsMaybeSignal}; use crate::core::{ElementMaybeSignal, ElementsMaybeSignal};
use crate::{watch_with_options, WatchOptions}; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use std::cell::RefCell;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::rc::Rc;
use wasm_bindgen::prelude::*; cfg_if! { if #[cfg(not(feature = "ssr"))] {
use crate::{watch_with_options, WatchOptions};
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
}}
/// Reactive [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver). /// Reactive [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver).
/// ///
@ -44,7 +48,7 @@ use wasm_bindgen::prelude::*;
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) /// On the server this amounts to a no-op.
/// ///
/// ## See also /// ## See also
/// ///
@ -66,6 +70,7 @@ where
} }
/// Version of [`use_intersection_observer`] that takes a [`UseIntersectionObserverOptions`]. See [`use_intersection_observer`] for how to use. /// Version of [`use_intersection_observer`] that takes a [`UseIntersectionObserverOptions`]. See [`use_intersection_observer`] for how to use.
#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
pub fn use_intersection_observer_with_options<El, T, RootEl, RootT, F>( pub fn use_intersection_observer_with_options<El, T, RootEl, RootT, F>(
target: El, target: El,
mut callback: F, mut callback: F,
@ -86,107 +91,113 @@ where
.. ..
} = options; } = options;
let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::IntersectionObserver)>::new(
move |entries: js_sys::Array, observer| {
callback(
entries
.to_vec()
.into_iter()
.map(|v| v.unchecked_into::<web_sys::IntersectionObserverEntry>())
.collect(),
observer,
);
},
)
.into_js_value();
let (is_active, set_active) = create_signal(immediate); let (is_active, set_active) = create_signal(immediate);
let observer: Rc<RefCell<Option<web_sys::IntersectionObserver>>> = Rc::new(RefCell::new(None)); cfg_if! { if #[cfg(feature = "ssr")] {
let pause = || {};
let cleanup = { let cleanup = || {};
let obsserver = Rc::clone(&observer); let stop = || {};
} else {
move || { let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::IntersectionObserver)>::new(
if let Some(o) = obsserver.take() { move |entries: js_sys::Array, observer| {
o.disconnect(); callback(
} entries
} .to_vec()
}; .into_iter()
.map(|v| v.unchecked_into::<web_sys::IntersectionObserverEntry>())
let targets = (target).into(); .collect(),
let root = root.map(|root| (root).into()); observer,
let stop_watch = {
let cleanup = cleanup.clone();
watch_with_options(
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),
) )
}; .into_js_value();
let stop = { let observer: Rc<RefCell<Option<web_sys::IntersectionObserver>>> = Rc::new(RefCell::new(None));
let cleanup = cleanup.clone();
move || { let cleanup = {
cleanup(); let obsserver = Rc::clone(&observer);
stop_watch();
}
};
on_cleanup(stop.clone()); move || {
if let Some(o) = obsserver.take() {
o.disconnect();
}
}
};
let pause = { let targets = (target).into();
let cleanup = cleanup.clone(); let root = root.map(|root| (root).into());
move || { let stop_watch = {
cleanup(); let cleanup = cleanup.clone();
set_active.set(false);
} watch_with_options(
}; 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 || {
cleanup();
stop_watch();
}
};
on_cleanup(stop.clone());
let pause = {
let cleanup = cleanup.clone();
move || {
cleanup();
set_active.set(false);
}
};
}}
UseIntersectionObserverReturn { UseIntersectionObserverReturn {
is_active: is_active.into(), is_active: is_active.into(),

View file

@ -1,14 +1,13 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::core::{ElementMaybeSignal, Position}; use crate::core::{ElementMaybeSignal, Position};
use crate::use_event_listener_with_options; use crate::{use_event_listener_with_options, UseEventListenerOptions};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::ev::{dragover, mousemove, touchend, touchmove, touchstart}; use leptos::ev::{dragover, mousemove, touchend, touchmove, touchstart};
use leptos::*; use leptos::*;
use std::marker::PhantomData; use std::marker::PhantomData;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use web_sys::AddEventListenerOptions;
/// Reactive mouse position /// Reactive mouse position
/// ///
@ -158,40 +157,39 @@ where
cfg_if! { if #[cfg(not(feature = "ssr"))] { cfg_if! { if #[cfg(not(feature = "ssr"))] {
let target = options.target; let target = options.target;
let mut event_listener_options = AddEventListenerOptions::new(); let event_listener_options = UseEventListenerOptions::default().passive(true);
event_listener_options.passive(true);
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
target.clone(), target.clone(),
mousemove, mousemove,
mouse_handler, mouse_handler,
event_listener_options.clone(), event_listener_options,
); );
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
target.clone(), target.clone(),
dragover, dragover,
drag_handler, drag_handler,
event_listener_options.clone(), event_listener_options,
); );
if options.touch && !matches!(options.coord_type, UseMouseCoordType::Movement) { if options.touch && !matches!(options.coord_type, UseMouseCoordType::Movement) {
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
target.clone(), target.clone(),
touchstart, touchstart,
touch_handler.clone(), touch_handler.clone(),
event_listener_options.clone(), event_listener_options,
); );
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
target.clone(), target.clone(),
touchmove, touchmove,
touch_handler, touch_handler,
event_listener_options.clone(), event_listener_options,
); );
if options.reset_on_touch_ends { if options.reset_on_touch_ends {
let _ = use_event_listener_with_options( let _ = use_event_listener_with_options(
target, target,
touchend, touchend,
move |_| reset(), move |_| reset(),
event_listener_options.clone(), event_listener_options,
); );
} }
} }

View file

@ -1,10 +1,14 @@
use crate::core::ElementsMaybeSignal; use crate::core::ElementsMaybeSignal;
use crate::use_supported; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use web_sys::MutationObserverInit;
cfg_if! { if #[cfg(not(feature = "ssr"))] {
use crate::use_supported;
use std::cell::RefCell;
use std::rc::Rc;
}}
/// Reactive [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver). /// Reactive [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
/// ///
@ -19,16 +23,13 @@ use web_sys::MutationObserverInit;
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::html::Pre; /// # use leptos::html::Pre;
/// # use leptos_use::use_mutation_observer_with_options; /// # use leptos_use::{use_mutation_observer_with_options, UseMutationObserverOptions};
/// # /// #
/// # #[component] /// # #[component]
/// # fn Demo() -> impl IntoView { /// # fn Demo() -> impl IntoView {
/// let el = create_node_ref::<Pre>(); /// let el = create_node_ref::<Pre>();
/// let (text, set_text) = create_signal("".to_string()); /// let (text, set_text) = create_signal("".to_string());
/// ///
/// let mut init = web_sys::MutationObserverInit::new();
/// init.attributes(true);
///
/// use_mutation_observer_with_options( /// use_mutation_observer_with_options(
/// el, /// el,
/// move |mutations, _| { /// move |mutations, _| {
@ -36,7 +37,7 @@ use web_sys::MutationObserverInit;
/// set_text.update(|text| *text = format!("{text}\n{:?}", mutation.attribute_name())); /// set_text.update(|text| *text = format!("{text}\n{:?}", mutation.attribute_name()));
/// } /// }
/// }, /// },
/// init, /// UseMutationObserverOptions::default().attributes(true),
/// ); /// );
/// ///
/// view! { /// view! {
@ -47,7 +48,7 @@ use web_sys::MutationObserverInit;
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) /// On the server this amounts to a no-op.
pub fn use_mutation_observer<El, T, F>( pub fn use_mutation_observer<El, T, F>(
target: El, target: El,
callback: F, callback: F,
@ -57,84 +58,167 @@ where
T: Into<web_sys::Element> + Clone + 'static, T: Into<web_sys::Element> + Clone + 'static,
F: FnMut(Vec<web_sys::MutationRecord>, web_sys::MutationObserver) + 'static, F: FnMut(Vec<web_sys::MutationRecord>, web_sys::MutationObserver) + 'static,
{ {
use_mutation_observer_with_options(target, callback, MutationObserverInit::default()) use_mutation_observer_with_options(target, callback, UseMutationObserverOptions::default())
} }
/// Version of [`use_mutation_observer`] that takes a `web_sys::MutationObserverInit`. See [`use_mutation_observer`] for how to use. /// Version of [`use_mutation_observer`] that takes a `UseMutationObserverOptions`. See [`use_mutation_observer`] for how to use.
#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
pub fn use_mutation_observer_with_options<El, T, F>( pub fn use_mutation_observer_with_options<El, T, F>(
target: El, target: El,
mut callback: F, mut callback: F,
options: web_sys::MutationObserverInit, options: UseMutationObserverOptions,
) -> UseMutationObserverReturn<impl Fn() + Clone> ) -> UseMutationObserverReturn<impl Fn() + Clone>
where where
El: Into<ElementsMaybeSignal<T, web_sys::Element>>, El: Into<ElementsMaybeSignal<T, web_sys::Element>>,
T: Into<web_sys::Element> + Clone + 'static, T: Into<web_sys::Element> + Clone + 'static,
F: FnMut(Vec<web_sys::MutationRecord>, web_sys::MutationObserver) + 'static, F: FnMut(Vec<web_sys::MutationRecord>, web_sys::MutationObserver) + 'static,
{ {
let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::MutationObserver)>::new( cfg_if! { if #[cfg(feature = "ssr")] {
move |entries: js_sys::Array, observer| { UseMutationObserverReturn {
callback( is_supported: Signal::derive(|| true),
entries stop: || {},
.to_vec()
.into_iter()
.map(|v| v.unchecked_into::<web_sys::MutationRecord>())
.collect(),
observer,
);
},
)
.into_js_value();
let observer: Rc<RefCell<Option<web_sys::MutationObserver>>> = Rc::new(RefCell::new(None));
let is_supported = use_supported(|| JsValue::from("MutationObserver").js_in(&window()));
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;
}
} }
}; } else {
let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::MutationObserver)>::new(
let targets = (target).into(); move |entries: js_sys::Array, observer| {
callback(
let stop_watch = { entries
let cleanup = cleanup.clone(); .to_vec()
.into_iter()
leptos::watch( .map(|v| v.unchecked_into::<web_sys::MutationRecord>())
move || targets.get(), .collect(),
move |targets, _, _| { observer,
cleanup(); );
if is_supported.get() && !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));
}
}, },
false,
) )
}; .into_js_value();
let stop = move || { let observer: Rc<RefCell<Option<web_sys::MutationObserver>>> = Rc::new(RefCell::new(None));
cleanup();
stop_watch();
};
on_cleanup(stop.clone()); let is_supported = use_supported(|| JsValue::from("MutationObserver").js_in(&window()));
UseMutationObserverReturn { is_supported, stop } 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 = (target).into();
let stop_watch = {
let cleanup = cleanup.clone();
leptos::watch(
move || targets.get(),
move |targets, _, _| {
cleanup();
if is_supported.get() && !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().into());
}
observer.replace(Some(obs));
}
},
false,
)
};
let stop = move || {
cleanup();
stop_watch();
};
on_cleanup(stop.clone());
UseMutationObserverReturn { is_supported, stop }
}}
}
/// Options for [`use_mutation_observer_with_options`].
#[derive(DefaultBuilder, Clone, Default)]
pub struct UseMutationObserverOptions {
/// Set to `true` to extend monitoring to the entire subtree of nodes rooted at `target`.
/// All of the other properties are then extended to all of the nodes in the subtree
/// instead of applying solely to the `target` node. The default value is `false`.
subtree: bool,
/// Set to `true` to monitor the target node (and, if `subtree` is `true`, its descendants)
/// for the addition of new child nodes or removal of existing child nodes.
/// The default value is `false`.
child_list: bool,
/// Set to `true` to watch for changes to the value of attributes on the node or nodes being
/// monitored. The default value is `true` if either of `attribute_filter` or
/// `attribute_old_value` is specified, otherwise the default value is `false`.
attributes: bool,
/// An array of specific attribute names to be monitored. If this property isn't included,
/// changes to all attributes cause mutation notifications.
#[builder(into)]
attribute_filter: Option<Vec<String>>,
/// Set to `true` to record the previous value of any attribute that changes when monitoring
/// the node or nodes for attribute changes; See
/// [Monitoring attribute values](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#monitoring_attribute_values)
/// for an example of watching for attribute changes and recording values.
/// The default value is `false`.
attribute_old_value: bool,
/// Set to `true` to monitor the specified target node
/// (and, if `subtree` is `true`, its descendants)
/// for changes to the character data contained within the node or nodes.
/// The default value is `true` if `character_data_old_value` is specified,
/// otherwise the default value is `false`.
#[builder(into)]
character_data: Option<bool>,
/// Set to `true` to record the previous value of a node's text whenever the text changes on
/// nodes being monitored. The default value is `false`.
character_data_old_value: bool,
}
impl From<UseMutationObserverOptions> for web_sys::MutationObserverInit {
fn from(val: UseMutationObserverOptions) -> Self {
let UseMutationObserverOptions {
subtree,
child_list,
attributes,
attribute_filter,
attribute_old_value,
character_data,
character_data_old_value,
} = val;
let mut init = Self::new();
init.subtree(subtree)
.child_list(child_list)
.attributes(attributes)
.attribute_old_value(attribute_old_value)
.character_data_old_value(character_data_old_value);
if let Some(attribute_filter) = attribute_filter {
let array = js_sys::Array::from_iter(attribute_filter.into_iter().map(JsValue::from));
init.attribute_filter(array.unchecked_ref());
}
if let Some(character_data) = character_data {
init.character_data(character_data);
}
init
}
} }
/// The return value of [`use_mutation_observer`]. /// The return value of [`use_mutation_observer`].

View file

@ -1,10 +1,14 @@
use crate::core::ElementsMaybeSignal; use crate::core::ElementsMaybeSignal;
use crate::use_supported; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use std::cell::RefCell;
use std::rc::Rc; cfg_if! { if #[cfg(not(feature = "ssr"))] {
use wasm_bindgen::prelude::*; use crate::use_supported;
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
}}
/// Reports changes to the dimensions of an Element's content or the border-box. /// Reports changes to the dimensions of an Element's content or the border-box.
/// ///
@ -45,7 +49,7 @@ use wasm_bindgen::prelude::*;
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) /// On the server this amounts to a no-op.
/// ///
/// ## See also /// ## See also
/// ///
@ -63,6 +67,7 @@ where
} }
/// Version of [`use_resize_observer`] that takes a `web_sys::ResizeObserverOptions`. See [`use_resize_observer`] for how to use. /// Version of [`use_resize_observer`] that takes a `web_sys::ResizeObserverOptions`. See [`use_resize_observer`] for how to use.
#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
pub fn use_resize_observer_with_options<El, T, F>( pub fn use_resize_observer_with_options<El, T, F>(
target: El, // TODO : multiple elements? target: El, // TODO : multiple elements?
mut callback: F, mut callback: F,
@ -73,92 +78,95 @@ where
T: Into<web_sys::Element> + Clone + 'static, T: Into<web_sys::Element> + Clone + 'static,
F: FnMut(Vec<web_sys::ResizeObserverEntry>, web_sys::ResizeObserver) + 'static, F: FnMut(Vec<web_sys::ResizeObserverEntry>, web_sys::ResizeObserver) + 'static,
{ {
let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::ResizeObserver)>::new( cfg_if! { if #[cfg(feature = "ssr")] {
move |entries: js_sys::Array, observer| { UseResizeObserverReturn {
callback( is_supported: Signal::derive(|| true),
entries stop: || {}
.to_vec()
.into_iter()
.map(|v| v.unchecked_into::<web_sys::ResizeObserverEntry>())
.collect(),
observer,
);
},
)
.into_js_value();
let observer: Rc<RefCell<Option<web_sys::ResizeObserver>>> = Rc::new(RefCell::new(None));
let is_supported = use_supported(|| JsValue::from("ResizeObserver").js_in(&window()));
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;
}
} }
}; } else {
let closure_js = Closure::<dyn FnMut(js_sys::Array, web_sys::ResizeObserver)>::new(
let targets = (target).into(); move |entries: js_sys::Array, observer| {
callback(
let stop_watch = { entries
let cleanup = cleanup.clone(); .to_vec()
.into_iter()
watch( .map(|v| v.unchecked_into::<web_sys::ResizeObserverEntry>())
move || targets.get(), .collect(),
move |targets, _, _| { observer,
cleanup(); );
if is_supported.get() && !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));
}
}, },
false,
) )
}; .into_js_value();
let stop = move || { let observer: Rc<RefCell<Option<web_sys::ResizeObserver>>> = Rc::new(RefCell::new(None));
cleanup();
stop_watch();
};
on_cleanup(stop.clone()); let is_supported = use_supported(|| JsValue::from("ResizeObserver").js_in(&window()));
UseResizeObserverReturn { is_supported, stop } 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 = (target).into();
let stop_watch = {
let cleanup = cleanup.clone();
watch(
move || targets.get(),
move |targets, _, _| {
cleanup();
if is_supported.get() && !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));
}
},
false,
)
};
let stop = move || {
cleanup();
stop_watch();
};
on_cleanup(stop.clone());
UseResizeObserverReturn { is_supported, stop }
}}
} }
/// Options for [`use_resize_observer_with_options`]. /// Options for [`use_resize_observer_with_options`].
#[derive(DefaultBuilder, Clone)] #[derive(DefaultBuilder, Clone, Default)]
pub struct UseResizeObserverOptions { pub struct UseResizeObserverOptions {
/// The box that is used to determine the dimensions of the target. Defaults to `ContentBox`. /// The box that is used to determine the dimensions of the target. Defaults to `ContentBox`.
pub box_: web_sys::ResizeObserverBoxOptions, #[builder(into)]
} pub box_: Option<web_sys::ResizeObserverBoxOptions>,
impl Default for UseResizeObserverOptions {
fn default() -> Self {
Self {
box_: web_sys::ResizeObserverBoxOptions::ContentBox,
}
}
} }
impl From<UseResizeObserverOptions> for web_sys::ResizeObserverOptions { impl From<UseResizeObserverOptions> for web_sys::ResizeObserverOptions {
fn from(val: UseResizeObserverOptions) -> Self { fn from(val: UseResizeObserverOptions) -> Self {
let mut options = web_sys::ResizeObserverOptions::new(); let mut options = web_sys::ResizeObserverOptions::new();
options.box_(val.box_); options.box_(
val.box_
.unwrap_or(web_sys::ResizeObserverBoxOptions::ContentBox),
);
options options
} }
} }

View file

@ -1,13 +1,25 @@
use crate::core::ElementMaybeSignal; use crate::core::ElementMaybeSignal;
use crate::use_event_listener::use_event_listener_with_options; use crate::UseEventListenerOptions;
use crate::{use_debounce_fn_with_arg, use_throttle_fn_with_arg_and_options, ThrottleOptions};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::ev::scrollend;
use leptos::*; use leptos::*;
use std::rc::Rc; use std::rc::Rc;
cfg_if! { if #[cfg(not(feature = "ssr"))] {
use crate::use_event_listener::use_event_listener_with_options;
use crate::{
use_debounce_fn_with_arg, use_throttle_fn_with_arg_and_options, ThrottleOptions,
};
use leptos::ev::scrollend;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
/// We have to check if the scroll amount is close enough to some threshold in order to
/// more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded
/// numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded.
/// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
const ARRIVED_STATE_THRESHOLD_PIXELS: f64 = 1.0;
}}
/// Reactive scroll position and state. /// Reactive scroll position and state.
/// ///
/// ## Demo /// ## Demo
@ -172,7 +184,7 @@ where
} }
/// Version of [`use_scroll`] with options. See [`use_scroll`] for how to use. /// Version of [`use_scroll`] with options. See [`use_scroll`] for how to use.
#[allow(unused_variables)] #[cfg_attr(feature = "ssr", allow(unused_variables))]
pub fn use_scroll_with_options<El, T>(element: El, options: UseScrollOptions) -> UseScrollReturn pub fn use_scroll_with_options<El, T>(element: El, options: UseScrollOptions) -> UseScrollReturn
where where
El: Clone, El: Clone,
@ -377,7 +389,7 @@ where
target, target,
ev::scroll, ev::scroll,
handler, handler,
options.event_listener_options.clone().unwrap_or_default(), options.event_listener_options,
); );
} else { } else {
let _ = use_event_listener_with_options::< let _ = use_event_listener_with_options::<
@ -389,7 +401,7 @@ where
target, target,
ev::scroll, ev::scroll,
on_scroll_handler, on_scroll_handler,
options.event_listener_options.clone().unwrap_or_default(), options.event_listener_options,
); );
} }
@ -402,7 +414,7 @@ where
target, target,
scrollend, scrollend,
on_scroll_end, on_scroll_end,
options.event_listener_options.unwrap_or_default(), options.event_listener_options,
); );
let measure = Box::new(move || { let measure = Box::new(move || {
@ -426,15 +438,10 @@ where
} }
} }
/// We have to check if the scroll amount is close enough to some threshold in order to
/// more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded
/// numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded.
/// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
const ARRIVED_STATE_THRESHOLD_PIXELS: f64 = 1.0;
/// Options for [`use_scroll`]. /// Options for [`use_scroll`].
#[derive(DefaultBuilder)] #[derive(DefaultBuilder)]
/// Options for [`use_scroll_with_options`]. /// Options for [`use_scroll_with_options`].
#[cfg_attr(feature = "ssr", allow(dead_code))]
pub struct UseScrollOptions { pub struct UseScrollOptions {
/// Throttle time in milliseconds for the scroll events. Defaults to 0 (disabled). /// Throttle time in milliseconds for the scroll events. Defaults to 0 (disabled).
throttle: f64, throttle: f64,
@ -453,8 +460,7 @@ pub struct UseScrollOptions {
on_stop: Rc<dyn Fn(web_sys::Event)>, on_stop: Rc<dyn Fn(web_sys::Event)>,
/// Options passed to the `addEventListener("scroll", ...)` call /// Options passed to the `addEventListener("scroll", ...)` call
#[builder(into)] event_listener_options: UseEventListenerOptions,
event_listener_options: Option<web_sys::AddEventListenerOptions>,
/// When changing the `x` or `y` signals this specifies the scroll behaviour. /// When changing the `x` or `y` signals this specifies the scroll behaviour.
/// Can be `Auto` (= not smooth) or `Smooth`. Defaults to `Auto`. /// Can be `Auto` (= not smooth) or `Smooth`. Defaults to `Auto`.

61
src/use_window.rs Normal file
View file

@ -0,0 +1,61 @@
use crate::{use_document, UseDocument};
use cfg_if::cfg_if;
use std::ops::Deref;
#[cfg(not(feature = "ssr"))]
use leptos::*;
/// SSR safe `window()`.
/// This returns just a new-type wrapper around `Option<Window>`.
/// Calling this amounts to `None` on the server and `Some(Window)` on the client.
///
/// It provides some convenient methods for working with the window like `document()` and `navigator()`.
/// These will all return `None` on the server.
///
/// ## Usage
///
/// ```
/// # use leptos::*;
/// # use leptos_use::use_window;
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// let window = use_window();
///
/// // Returns `None` on the server but will not panic.
/// let navigator = window.navigator();
/// #
/// # view! { }
/// # }
/// ```
pub fn use_window() -> UseWindow {
cfg_if! { if #[cfg(feature = "ssr")] {
UseWindow(None)
} else {
UseWindow(Some(window()))
}}
}
/// Return type of [`use_window`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UseWindow(Option<web_sys::Window>);
impl Deref for UseWindow {
type Target = Option<web_sys::Window>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl UseWindow {
/// Returns the `Some(Navigator)` in the Browser. `None` otherwise.
pub fn navigator(&self) -> Option<web_sys::Navigator> {
self.0.as_ref().map(|w| w.navigator())
}
/// Returns the same as [`use_document`].
#[inline(always)]
pub fn document(&self) -> UseDocument {
use_document()
}
}

View file

@ -1,10 +1,9 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::use_event_listener_with_options; use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use leptos::ev::scroll; use leptos::ev::scroll;
use leptos::*; use leptos::*;
use web_sys::AddEventListenerOptions;
/// Reactive window scroll. /// Reactive window scroll.
/// ///
@ -40,21 +39,17 @@ pub fn use_window_scroll() -> (Signal<f64>, Signal<f64>) {
let (x, set_x) = create_signal(initial_x); let (x, set_x) = create_signal(initial_x);
let (y, set_y) = create_signal(initial_y); let (y, set_y) = create_signal(initial_y);
cfg_if! { if #[cfg(not(feature = "ssr"))] { let _ = use_event_listener_with_options(
let mut options = AddEventListenerOptions::new(); use_window(),
options.capture(false); scroll,
options.passive(true); move |_| {
set_x.set(window().scroll_x().unwrap_or_default());
let _ = use_event_listener_with_options( set_y.set(window().scroll_y().unwrap_or_default());
window(), },
scroll, UseEventListenerOptions::default()
move |_| { .capture(false)
set_x.set(window().scroll_x().unwrap_or_default()); .passive(true),
set_y.set(window().scroll_y().unwrap_or_default()); );
},
options,
);
}}
(x.into(), y.into()) (x.into(), y.into())
} }

View file

@ -12,6 +12,7 @@ use leptos::*;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::watch_debounced; /// # use leptos_use::watch_debounced;
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {
@ -35,6 +36,7 @@ use leptos::*;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::{watch_debounced_with_options, WatchDebouncedOptions}; /// # use leptos_use::{watch_debounced_with_options, WatchDebouncedOptions};
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {

View file

@ -11,6 +11,7 @@ use leptos::*;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::{watch_pausable, WatchPausableReturn}; /// # use leptos_use::{watch_pausable, WatchPausableReturn};
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {

View file

@ -11,6 +11,7 @@ use default_struct_builder::DefaultBuilder;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::watch_throttled; /// # use leptos_use::watch_throttled;
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {
@ -34,6 +35,7 @@ use default_struct_builder::DefaultBuilder;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::{watch_throttled_with_options, WatchThrottledOptions}; /// # use leptos_use::{watch_throttled_with_options, WatchThrottledOptions};
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {

View file

@ -17,6 +17,7 @@ use std::rc::Rc;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::{watch_with_options, WatchOptions}; /// # use leptos_use::{watch_with_options, WatchOptions};
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {
@ -41,6 +42,7 @@ use std::rc::Rc;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::{watch_with_options, WatchOptions}; /// # use leptos_use::{watch_with_options, WatchOptions};
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {
@ -59,6 +61,7 @@ use std::rc::Rc;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::{watch_with_options, WatchOptions}; /// # use leptos_use::{watch_with_options, WatchOptions};
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {

View file

@ -6,6 +6,7 @@ use crate::{watch_with_options, WatchOptions};
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::whenever; /// # use leptos_use::whenever;
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {
@ -23,6 +24,7 @@ use crate::{watch_with_options, WatchOptions};
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::whenever; /// # use leptos_use::whenever;
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {
@ -41,6 +43,7 @@ use crate::{watch_with_options, WatchOptions};
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::whenever; /// # use leptos_use::whenever;
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {
@ -60,6 +63,7 @@ use crate::{watch_with_options, WatchOptions};
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos::logging::log;
/// # use leptos_use::{WatchOptions, whenever_with_options}; /// # use leptos_use::{WatchOptions, whenever_with_options};
/// # /// #
/// # pub fn Demo() -> impl IntoView { /// # pub fn Demo() -> impl IntoView {