diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index 466d865..39d643e 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -53,6 +53,7 @@ + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..53e6b55 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 658011e..21005d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - +## [Unreleased] - ### New Functions 🚀 +- `use_document` +- `use_window` +- `use_geolocation` - `use_webtransport` - `signal_debounced` - `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 - 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. -- `watch` is now removed in favor of `leptos::watch` and will be removed in a future release. -`watch_with_options` will continue to exist. +- `watch` is now deprecated in favor of `leptos::watch` and will be removed in a future release. + `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`: - - 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 returned signals `ready_state`, `message`, `message_bytes` have now the type - `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` to improve DX. - - 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`). + - 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 returned signals `ready_state`, `message`, `message_bytes` have now the type + `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` to improve DX. + - 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`). - `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. - 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 - 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 🍕 @@ -82,11 +100,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `use_local_storage` - `use_session_storage` - Instead of returning `ReadSignal`, the following functions now return `Signal`. - - `use_color_mode` - - `use_favicon` - - `use_storage` - - `use_local_storage` - - `use_session_storage` + - `use_color_mode` + - `use_favicon` + - `use_storage` + - `use_local_storage` + - `use_session_storage` ### Fixes 🍕 diff --git a/Cargo.toml b/Cargo.toml index 7a98094..55ccc8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos-use" -version = "0.7.0-beta" +version = "0.7.0-rc" edition = "2021" authors = ["Marc-Stefan Cassola"] categories = ["gui", "web-programming"] @@ -13,7 +13,7 @@ repository = "https://github.com/Synphonyte/leptos-use" homepage = "https://leptos-use.rs" [dependencies] -leptos = "0.5.0-beta2" +leptos = "0.5.0-rc1" wasm-bindgen = "0.2" js-sys = "0.3" default-struct-builder = "0.5" @@ -33,8 +33,9 @@ version = "0.3" features = [ "AddEventListenerOptions", "BinaryType", - "CssStyleDeclaration", + "Coordinates", "CloseEvent", + "CssStyleDeclaration", "CustomEvent", "CustomEventInit", "DomRect", @@ -47,6 +48,7 @@ features = [ "EventTarget", "File", "FileList", + "Geolocation", "HtmlElement", "HtmlLinkElement", "HtmlStyleElement", @@ -61,6 +63,9 @@ features = [ "Navigator", "NodeList", "PointerEvent", + "Position", + "PositionError", + "PositionOptions", "ReadableStream", "ReadableStreamDefaultReader", "ReadableStreamGetReaderOptions", diff --git a/README.md b/README.md index 76027cd..16b0401 100644 --- a/README.md +++ b/README.md @@ -90,4 +90,4 @@ To scaffold a new function quickly you can run `template/createfn.sh`. It requir |---------------|---------------------------| | <= 0.3 | 0.3 | | 0.4, 0.5, 0.6 | 0.4 | -| main | 0.5.0-alpha/beta | +| main | 0.5.0-alpha/beta/rc | diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index f98b3f7..38d67ab 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -15,6 +15,7 @@ # Elements - [use_active_element](elements/use_active_element.md) +- [use_document](elements/use_document.md) - [use_document_visibility](elements/use_document_visibility.md) - [use_draggable](elements/use_draggable.md) - [use_drop_zone](elements/use_drop_zone.md) @@ -23,6 +24,7 @@ - [use_intersection_observer](elements/use_intersection_observer.md) - [use_mutation_observer](elements/use_mutation_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_scroll](elements/use_window_scroll.md) @@ -41,6 +43,7 @@ - [on_click_outside](sensors/on_click_outside.md) - [use_element_hover](sensors/use_element_hover.md) +- [use_geolocation](sensors/use_geolocation.md) - [use_mouse](sensors/use_mouse.md) - [use_scroll](sensors/use_scroll.md) diff --git a/docs/book/src/elements/use_document.md b/docs/book/src/elements/use_document.md new file mode 100644 index 0000000..838ae90 --- /dev/null +++ b/docs/book/src/elements/use_document.md @@ -0,0 +1,3 @@ +# use_document + + diff --git a/docs/book/src/elements/use_window.md b/docs/book/src/elements/use_window.md new file mode 100644 index 0000000..ee5e14a --- /dev/null +++ b/docs/book/src/elements/use_window.md @@ -0,0 +1,3 @@ +# use_window + + diff --git a/docs/book/src/sensors/use_geolocation.md b/docs/book/src/sensors/use_geolocation.md new file mode 100644 index 0000000..f27d246 --- /dev/null +++ b/docs/book/src/sensors/use_geolocation.md @@ -0,0 +1,3 @@ +# use_geolocation + + diff --git a/docs/book/src/server_side_rendering.md b/docs/book/src/server_side_rendering.md index 8f16416..278c806 100644 --- a/docs/book/src/server_side_rendering.md +++ b/docs/book/src/server_side_rendering.md @@ -32,18 +32,23 @@ and the server. ## 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 -available. This is not the case on the server. You can simply wrap them in `create_effect` which will cause them to -be only called in the browser. +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 always the case on the server. If you use them with `NodeRefs` they will just work in SSR. +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` or `Option` respectively. These can be +used safely on the server. The following code works on both the client and the server: ```rust -create_effect( - cx, - move |_| { - // window() doesn't work on the server - use_event_listener(window(), "resize", move |_| { - // ... - }) - }, -); -``` \ No newline at end of file +use leptos::*; +use leptos::ev::keyup; +use leptos_use::{use_event_listener, use_window}; + +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. \ No newline at end of file diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 230afb4..8a47395 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -22,6 +22,7 @@ members = [ "use_event_listener", "use_favicon", "use_floor", + "use_geolocation", "use_intersection_observer", "use_interval", "use_interval_fn", diff --git a/examples/signal_debounced/src/main.rs b/examples/signal_debounced/src/main.rs index 9b1ab14..4e42d6c 100644 --- a/examples/signal_debounced/src/main.rs +++ b/examples/signal_debounced/src/main.rs @@ -5,7 +5,7 @@ use leptos_use::signal_debounced; #[component] fn Demo() -> impl IntoView { let (input, set_input) = create_signal("".to_string()); - let debounced = signal_debounced(input, 1000.0); + let debounced: Signal = signal_debounced(input, 1000.0); view! {
diff --git a/examples/signal_throttled/src/main.rs b/examples/signal_throttled/src/main.rs index 0fc43ac..2ff7366 100644 --- a/examples/signal_throttled/src/main.rs +++ b/examples/signal_throttled/src/main.rs @@ -5,7 +5,7 @@ use leptos_use::signal_throttled; #[component] fn Demo() -> impl IntoView { let (input, set_input) = create_signal("".to_string()); - let throttled = signal_throttled(input, 1000.0); + let throttled: Signal = signal_throttled(input, 1000.0); view! {
diff --git a/examples/ssr/Cargo.toml b/examples/ssr/Cargo.toml index 15773d7..d782c10 100644 --- a/examples/ssr/Cargo.toml +++ b/examples/ssr/Cargo.toml @@ -11,16 +11,16 @@ axum = { version = "0.6.4", optional = true } console_error_panic_hook = "0.1" console_log = "1" cfg-if = "1" -leptos = { version = "0.5.0-beta2", features = ["nightly"] } -leptos_axum = { version = "0.5.0-beta2", optional = true } -leptos_meta = { version = "0.5.0-beta2", features = ["nightly"] } -leptos_router = { version = "0.5.0-beta2", features = ["nightly"] } +leptos = { version = "0.5.0-rc1", features = ["nightly"] } +leptos_axum = { version = "0.5.0-rc1", optional = true } +leptos_meta = { version = "0.5.0-rc1", features = ["nightly"] } +leptos_router = { version = "0.5.0-rc1", features = ["nightly"] } leptos-use = { path = "../..", features = ["storage"] } log = "0.4" simple_logger = "4" tokio = { version = "1.25.0", 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" thiserror = "1.0.38" tracing = { version = "0.1.37", optional = true } diff --git a/examples/ssr/src/app.rs b/examples/ssr/src/app.rs index 55e0066..29e62fc 100644 --- a/examples/ssr/src/app.rs +++ b/examples/ssr/src/app.rs @@ -5,7 +5,8 @@ use leptos_meta::*; use leptos_router::*; use leptos_use::storage::use_local_storage; 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] @@ -47,11 +48,9 @@ fn HomePage() -> impl IntoView { let (key, set_key) = create_signal("".to_string()); - create_effect(move |_| { - // window() doesn't work on the server - let _ = use_event_listener(window(), keypress, move |evt: KeyboardEvent| { - set_key(evt.key()) - }); + // window() doesn't work on the server so we provide use_window() + let _ = use_event_listener(use_window(), keypress, move |evt: KeyboardEvent| { + set_key(evt.key()) }); let (debounce_value, set_debounce_value) = create_signal("not called"); @@ -65,10 +64,10 @@ fn HomePage() -> impl IntoView { debounced_fn(); view! { -

"Leptos-Use SSR Example"

- -

"Locale "zh-Hans-CN-u-nu-hanidec": " {zh_count}

-

"Press any key: " {key}

-

"Debounced called: " {debounce_value}

+

Leptos-Use SSR Example

+ +

Locale zh-Hans-CN-u-nu-hanidec: {zh_count}

+

Press any key: {key}

+

Debounced called: {debounce_value}

} } diff --git a/examples/ssr/src/main.rs b/examples/ssr/src/main.rs index a9dfaa5..6b92404 100644 --- a/examples/ssr/src/main.rs +++ b/examples/ssr/src/main.rs @@ -2,6 +2,7 @@ #[tokio::main] async fn main() { use axum::{routing::post, Router}; + use leptos::logging::log; use leptos::*; use leptos_axum::{generate_route_list, LeptosRoutes}; use start_axum::app::*; diff --git a/examples/use_event_listener/src/main.rs b/examples/use_event_listener/src/main.rs index 067c17e..eb2b22d 100644 --- a/examples/use_event_listener/src/main.rs +++ b/examples/use_event_listener/src/main.rs @@ -1,5 +1,6 @@ use leptos::ev::{click, keydown}; use leptos::html::A; +use leptos::logging::log; use leptos::*; use leptos_use::use_event_listener; diff --git a/examples/use_geolocation/Cargo.toml b/examples/use_geolocation/Cargo.toml new file mode 100644 index 0000000..d1e017f --- /dev/null +++ b/examples/use_geolocation/Cargo.toml @@ -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" diff --git a/examples/use_geolocation/README.md b/examples/use_geolocation/README.md new file mode 100644 index 0000000..b2d630a --- /dev/null +++ b/examples/use_geolocation/README.md @@ -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 +``` \ No newline at end of file diff --git a/examples/use_geolocation/Trunk.toml b/examples/use_geolocation/Trunk.toml new file mode 100644 index 0000000..3e4be08 --- /dev/null +++ b/examples/use_geolocation/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "/demo/" \ No newline at end of file diff --git a/examples/use_geolocation/index.html b/examples/use_geolocation/index.html new file mode 100644 index 0000000..ae249a6 --- /dev/null +++ b/examples/use_geolocation/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/use_geolocation/input.css b/examples/use_geolocation/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/examples/use_geolocation/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/use_geolocation/rust-toolchain.toml b/examples/use_geolocation/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/examples/use_geolocation/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/examples/use_geolocation/src/main.rs b/examples/use_geolocation/src/main.rs new file mode 100644 index 0000000..c4e70f7 --- /dev/null +++ b/examples/use_geolocation/src/main.rs @@ -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! { +
+    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()}},
+                
+ + + } +} + +fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + mount_to(demo_or_body(), || { + view! { } + }) +} diff --git a/examples/use_geolocation/style/output.css b/examples/use_geolocation/style/output.css new file mode 100644 index 0000000..ab5191f --- /dev/null +++ b/examples/use_geolocation/style/output.css @@ -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)); + } +} \ No newline at end of file diff --git a/examples/use_geolocation/tailwind.config.js b/examples/use_geolocation/tailwind.config.js new file mode 100644 index 0000000..bc09f5e --- /dev/null +++ b/examples/use_geolocation/tailwind.config.js @@ -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'), + ], +} \ No newline at end of file diff --git a/examples/use_mutation_observer/src/main.rs b/examples/use_mutation_observer/src/main.rs index 2d06506..db7cba4 100644 --- a/examples/use_mutation_observer/src/main.rs +++ b/examples/use_mutation_observer/src/main.rs @@ -1,7 +1,7 @@ use leptos::html::Div; use leptos::*; 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; #[component] @@ -11,9 +11,6 @@ fn Demo() -> impl IntoView { let (class_name, set_class_name) = 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( el, move |mutations, _| { @@ -23,7 +20,7 @@ fn Demo() -> impl IntoView { }); } }, - init, + UseMutationObserverOptions::default().attributes(true), ); let _ = set_timeout_with_handle( diff --git a/examples/use_scroll/src/main.rs b/examples/use_scroll/src/main.rs index cd01592..a70b87e 100644 --- a/examples/use_scroll/src/main.rs +++ b/examples/use_scroll/src/main.rs @@ -1,4 +1,5 @@ use leptos::html::Div; +use leptos::logging::log; use leptos::*; use leptos_use::docs::{demo_or_body, BooleanDisplay}; use leptos_use::{use_scroll_with_options, ScrollBehavior, UseScrollOptions, UseScrollReturn}; diff --git a/src/core/element_maybe_signal.rs b/src/core/element_maybe_signal.rs index c368297..9d972a7 100644 --- a/src/core/element_maybe_signal.rs +++ b/src/core/element_maybe_signal.rs @@ -1,3 +1,4 @@ +use crate::{UseDocument, UseWindow}; use leptos::html::ElementDescriptor; use leptos::*; use std::marker::PhantomData; @@ -153,6 +154,22 @@ where } } +macro_rules! impl_from_deref_option { + ($ty:ty, $ty2:ty) => { + impl 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) /////////////////////////////////////////////////////////////// impl<'a, E> From<&'a str> for ElementMaybeSignal diff --git a/src/core/elements_maybe_signal.rs b/src/core/elements_maybe_signal.rs index 645d377..8f5eff3 100644 --- a/src/core/elements_maybe_signal.rs +++ b/src/core/elements_maybe_signal.rs @@ -1,4 +1,5 @@ use crate::core::ElementMaybeSignal; +use crate::{UseDocument, UseWindow}; use leptos::html::ElementDescriptor; use leptos::*; use std::marker::PhantomData; @@ -154,6 +155,22 @@ where } } +macro_rules! impl_from_deref_option { + ($ty:ty, $ty2:ty) => { + impl 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) /////////////////////////////////////////////////////////////// impl<'a, E> From<&'a str> for ElementsMaybeSignal diff --git a/src/lib.rs b/src/lib.rs index c322d6f..d589bba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,9 @@ mod is_none; mod is_ok; mod is_some; mod on_click_outside; +mod use_document; +mod use_window; +mod use_geolocation; mod signal_debounced; mod signal_throttled; mod use_active_element; @@ -70,6 +73,9 @@ pub use is_none::*; pub use is_ok::*; pub use is_some::*; pub use on_click_outside::*; +pub use use_document::*; +pub use use_window::*; +pub use use_geolocation::*; pub use signal_debounced::*; pub use signal_throttled::*; pub use use_active_element::*; diff --git a/src/on_click_outside.rs b/src/on_click_outside.rs index fcce00d..1ac1cca 100644 --- a/src/on_click_outside.rs +++ b/src/on_click_outside.rs @@ -1,17 +1,20 @@ use crate::core::{ElementMaybeSignal, ElementsMaybeSignal}; -use crate::utils::IS_IOS; -use crate::{use_event_listener, use_event_listener_with_options}; +use cfg_if::cfg_if; 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 = 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 = RwLock::new(false); +}} /// Listen for clicks outside of an element. /// Useful for modals or dropdowns. @@ -25,6 +28,7 @@ static IOS_WORKAROUND: RwLock = RwLock::new(false); /// ``` /// # use leptos::*; /// # use leptos::ev::resize; +/// # use leptos::logging::log; /// # use leptos::html::Div; /// # use leptos_use::on_click_outside; /// # @@ -48,7 +52,7 @@ static IOS_WORKAROUND: RwLock = RwLock::new(false); /// /// ## 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(target: El, handler: F) -> impl FnOnce() + Clone where El: Clone, @@ -64,6 +68,7 @@ where } /// 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( target: El, handler: F, @@ -76,151 +81,151 @@ where F: FnMut(web_sys::Event) + Clone + 'static, I: Into + Clone + 'static, { - let OnClickOutsideOptions { - ignore, - capture, - detect_iframes, - } = options; + cfg_if! { if #[cfg(feature = "ssr")] { + || {} + } else { + let OnClickOutsideOptions { + ignore, + capture, + detect_iframes, + } = options; - // Fixes: https://github.com/vueuse/vueuse/issues/1520 - // How it works: https://stackoverflow.com/a/39712411 - if *IS_IOS { - if let Ok(mut ios_workaround) = IOS_WORKAROUND.write() { - if !*ios_workaround { - *ios_workaround = true; - if let Some(body) = document().body() { - let children = body.children(); - for i in 0..children.length() { - let _ = children - .get_with_index(i) - .expect("checked index") - .add_event_listener_with_callback( - "click", - &js_sys::Function::default(), - ); + // Fixes: https://github.com/vueuse/vueuse/issues/1520 + // How it works: https://stackoverflow.com/a/39712411 + if *IS_IOS { + if let Ok(mut ios_workaround) = IOS_WORKAROUND.write() { + if !*ios_workaround { + *ios_workaround = true; + if let Some(body) = document().body() { + let children = body.children(); + for i in 0..children.length() { + let _ = children + .get_with_index(i) + .expect("checked index") + .add_event_listener_with_callback( + "click", + &js_sys::Function::default(), + ); + } } } } } - } - let should_listen = Rc::new(Cell::new(true)); + let should_listen = Rc::new(Cell::new(true)); - let should_ignore = move |event: &web_sys::UiEvent| { - let ignore = ignore.get_untracked(); + let should_ignore = move |event: &web_sys::UiEvent| { + let ignore = ignore.get_untracked(); - ignore.into_iter().flatten().any(|element| { - let element: web_sys::EventTarget = element.into(); + ignore.into_iter().flatten().any(|element| { + let element: web_sys::EventTarget = element.into(); - event_target::(event) == element - || event.composed_path().includes(element.as_ref(), 0) - }) - }; + event_target::(event) == element + || event.composed_path().includes(element.as_ref(), 0) + }) + }; - let target = (target).into(); + let target = (target).into(); - let listener = { - let should_listen = Rc::clone(&should_listen); - let mut handler = handler.clone(); - let target = target.clone(); - let should_ignore = should_ignore.clone(); + let listener = { + let should_listen = Rc::clone(&should_listen); + let mut handler = handler.clone(); + let target = target.clone(); + let should_ignore = should_ignore.clone(); - move |event: web_sys::UiEvent| { - if let Some(el) = target.get_untracked() { - let el = el.into(); - - if el == event_target(&event) || event.composed_path().includes(el.as_ref(), 0) { - return; - } - - if event.detail() == 0 { - should_listen.set(!should_ignore(&event)); - } - - if !should_listen.get() { - should_listen.set(true); - return; - } - - handler(event.into()); - } - } - }; - - let remove_click_listener = { - let mut listener = listener.clone(); - - let mut options = AddEventListenerOptions::default(); - options.passive(true).capture(capture); - - use_event_listener_with_options::<_, web_sys::Window, _, _>( - 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| { + move |event: web_sys::UiEvent| { if let Some(el) = target.get_untracked() { - should_listen.set( - !event.composed_path().includes(el.into().as_ref(), 0) - && !should_ignore(&event), - ); + 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()); } - }, - options, - ) - }; + } + }; - 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 remove_click_listener = { + let mut listener = listener.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::() - .contains(Some(&active_element.into())) - { - handler(event.into()); + use_event_listener_with_options::<_, web_sys::Window, _, _>( + window(), + click, + move |event| listener(event.into()), + UseEventListenerOptions::default() + .passive(true) + .capture(capture), + ) + }; + + 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::() + .contains(Some(&active_element.into())) + { + handler(event.into()); + } } } - } - }, - Duration::ZERO, - ); - }, - )) - } else { - None - }; + }, + Duration::ZERO, + ); + }, + )) + } else { + None + }; - move || { - remove_click_listener(); - remove_pointer_listener(); - if let Some(f) = remove_blur_listener { - f(); + move || { + remove_click_listener(); + remove_pointer_listener(); + if let Some(f) = remove_blur_listener { + f(); + } } - } + }} } /// Options for [`on_click_outside_with_options`]. @@ -230,6 +235,7 @@ where T: Into + Clone + 'static, { /// List of elementss that should not trigger the callback. Defaults to `[]`. + #[cfg_attr(feature = "ssr", allow(dead_code))] ignore: ElementsMaybeSignal, /// Use capturing phase for internal event listener. Defaults to `true`. diff --git a/src/use_active_element.rs b/src/use_active_element.rs index 98b8d0f..4a671b0 100644 --- a/src/use_active_element.rs +++ b/src/use_active_element.rs @@ -1,11 +1,9 @@ #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] -use crate::use_event_listener_with_options; -use cfg_if::cfg_if; +use crate::{use_document, use_event_listener_with_options, UseEventListenerOptions}; use leptos::ev::{blur, focus}; use leptos::html::{AnyElement, ToHtmlElement}; use leptos::*; -use web_sys::AddEventListenerOptions; /// Reactive `document.activeElement` /// @@ -17,6 +15,7 @@ use web_sys::AddEventListenerOptions; /// /// ``` /// # use leptos::*; +/// # use leptos::logging::log; /// use leptos_use::use_active_element; /// # /// # #[component] @@ -35,44 +34,37 @@ use web_sys::AddEventListenerOptions; /// /// On the server this returns a `Signal` that always contains the value `None`. pub fn use_active_element() -> Signal>> { - cfg_if! { if #[cfg(feature = "ssr")] { - let get_active_element = || { None }; - } else { - let get_active_element = move || { - document() - .active_element() - .map(|el| el.to_leptos_element()) - }; - }} + let get_active_element = move || { + use_document() + .active_element() + .map(|el| el.to_leptos_element()) + }; let (active_element, set_active_element) = create_signal(get_active_element()); - cfg_if! { if #[cfg(not(feature = "ssr"))] { - let mut listener_options = AddEventListenerOptions::new(); - listener_options.capture(true); + let listener_options = UseEventListenerOptions::default().capture(true); - let _ = use_event_listener_with_options( - window(), - blur, - move |event| { - if event.related_target().is_some() { - return; - } + let _ = use_event_listener_with_options( + window(), + blur, + move |event| { + if event.related_target().is_some() { + return; + } - set_active_element.update(|el| *el = get_active_element()); - }, - listener_options.clone(), - ); + set_active_element.update(|el| *el = get_active_element()); + }, + listener_options, + ); - let _ = use_event_listener_with_options( - window(), - focus, - move |_| { - set_active_element.update(|el| *el = get_active_element()); - }, - listener_options, - ); - }} + let _ = use_event_listener_with_options( + window(), + focus, + move |_| { + set_active_element.update(|el| *el = get_active_element()); + }, + listener_options, + ); active_element.into() } diff --git a/src/use_breakpoints.rs b/src/use_breakpoints.rs index 0f6da51..8457d1c 100644 --- a/src/use_breakpoints.rs +++ b/src/use_breakpoints.rs @@ -1,4 +1,5 @@ use crate::use_media_query; +use leptos::logging::error; use leptos::*; use paste::paste; use std::collections::HashMap; diff --git a/src/use_css_var.rs b/src/use_css_var.rs index 64185f8..41f1d04 100644 --- a/src/use_css_var.rs +++ b/src/use_css_var.rs @@ -1,13 +1,16 @@ #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] 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 default_struct_builder::DefaultBuilder; use leptos::*; use std::marker::PhantomData; use std::time::Duration; -use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen::JsCast; /// Manipulate CSS variables. /// @@ -127,17 +130,14 @@ where }; if observe { - let mut init = web_sys::MutationObserverInit::new(); let update_css_var = update_css_var.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::, T, _>( el_signal, move |_, _| update_css_var(), - init, + UseMutationObserverOptions::default() + .attribute_filter(vec!["style".to_string()]), ); } diff --git a/src/use_cycle_list.rs b/src/use_cycle_list.rs index 9f094f0..ecbb6b5 100644 --- a/src/use_cycle_list.rs +++ b/src/use_cycle_list.rs @@ -12,6 +12,7 @@ use leptos::*; /// /// ``` /// # use leptos::*; +/// # use leptos::logging::log; /// use leptos_use::{use_cycle_list, UseCycleListReturn}; /// # /// # #[component] diff --git a/src/use_document.rs b/src/use_document.rs new file mode 100644 index 0000000..2a7a1bb --- /dev/null +++ b/src/use_document.rs @@ -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`. +/// 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); + +impl Deref for UseDocument { + type Target = Option; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl UseDocument { + pub fn body(&self) -> Option { + self.0.as_ref().and_then(|d| d.body()) + } + + pub fn active_element(&self) -> Option { + self.0.as_ref().and_then(|d| d.active_element()) + } +} diff --git a/src/use_draggable.rs b/src/use_draggable.rs index c882949..b293626 100644 --- a/src/use_draggable.rs +++ b/src/use_draggable.rs @@ -1,5 +1,5 @@ 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 leptos::ev::{pointerdown, pointermove, pointerup}; use leptos::*; @@ -183,20 +183,19 @@ where handle_event(event); }; - let mut listener_options = web_sys::AddEventListenerOptions::new(); - listener_options.capture(true); + let listener_options = UseEventListenerOptions::default().capture(true); let _ = use_event_listener_with_options( dragging_handle, pointerdown, on_pointer_down, - listener_options.clone(), + listener_options, ); let _ = use_event_listener_with_options( dragging_element.clone(), pointermove, on_pointer_move, - listener_options.clone(), + listener_options, ); let _ = use_event_listener_with_options( dragging_element, diff --git a/src/use_drop_zone.rs b/src/use_drop_zone.rs index 164c152..d5e778d 100644 --- a/src/use_drop_zone.rs +++ b/src/use_drop_zone.rs @@ -1,11 +1,15 @@ use crate::core::ElementMaybeSignal; -use crate::use_event_listener; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; -use leptos::ev::{dragenter, dragleave, dragover, drop}; use leptos::*; use std::fmt::{Debug, Formatter}; 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. /// /// ## Demo @@ -45,7 +49,8 @@ use std::rc::Rc; /// /// ## 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(target: El) -> UseDropZoneReturn where El: Clone, @@ -56,6 +61,7 @@ where } /// 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( target: El, options: UseDropZoneOptions, @@ -65,81 +71,83 @@ where El: Into>, T: Into + 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 (files, set_files) = create_signal(Vec::::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| { - if let Some(data_transfer) = event.data_transfer() { - let files: Vec = data_transfer - .files() - .map(|f| js_sys::Array::from(&f).to_vec()) - .unwrap_or_default() - .into_iter() - .map(web_sys::File::from) - .collect(); + let counter = store_value(0_usize); - 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 = 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| { - event.prevent_default(); - counter.update_value(|counter| *counter += 1); - set_over_drop_zone.set(true); + set_files.update(move |f| *f = files); + } + }; - 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 { - files: files.get_untracked(), - event, + update_files(&event); + + on_enter(UseDropZoneEvent { + files: files.get_untracked(), + event, + }); }); - }); - let _ = use_event_listener(target.clone(), dragover, move |event| { - event.prevent_default(); - update_files(&event); - on_over(UseDropZoneEvent { - files: files.get_untracked(), - event, + let _ = use_event_listener(target.clone(), dragover, move |event| { + event.prevent_default(); + update_files(&event); + on_over(UseDropZoneEvent { + files: files.get_untracked(), + event, + }); }); - }); - let _ = use_event_listener(target.clone(), dragleave, move |event| { - event.prevent_default(); - counter.update_value(|counter| *counter -= 1); - if counter.get_value() == 0 { + let _ = use_event_listener(target.clone(), dragleave, move |event| { + event.prevent_default(); + counter.update_value(|counter| *counter -= 1); + 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); - } - update_files(&event); + update_files(&event); - on_leave(UseDropZoneEvent { - files: files.get_untracked(), - event, + on_drop(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); - - update_files(&event); - - on_drop(UseDropZoneEvent { - files: files.get_untracked(), - event, - }); - }); + }} UseDropZoneReturn { files: files.into(), @@ -149,6 +157,7 @@ where /// Options for [`use_drop_zone_with_options`]. #[derive(DefaultBuilder, Clone)] +#[cfg_attr(feature = "ssr", allow(dead_code))] pub struct UseDropZoneOptions { /// Event handler for the [`drop`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event) event on_drop: Rc, diff --git a/src/use_element_hover.rs b/src/use_element_hover.rs index 3b8be30..4b3b412 100644 --- a/src/use_element_hover.rs +++ b/src/use_element_hover.rs @@ -1,11 +1,14 @@ 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 leptos::ev::{mouseenter, mouseleave}; use leptos::leptos_dom::helpers::TimeoutHandle; 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. /// @@ -33,7 +36,7 @@ use web_sys::AddEventListenerOptions; /// /// ## 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: El) -> Signal where El: Clone, @@ -45,6 +48,7 @@ where /// 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: El, options: UseElementHoverOptions, @@ -64,31 +68,32 @@ where let mut timer: Option = None; 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() { - handle.clear(); - } + if let Some(handle) = timer.take() { + handle.clear(); + } - if delay > 0 { - timer = set_timeout_with_handle( - move || set_hovered.set(entering), - Duration::from_millis(delay), - ) - .ok(); - } else { - set_hovered.set(entering); - } + if delay > 0 { + timer = set_timeout_with_handle( + move || set_hovered.set(entering), + Duration::from_millis(delay), + ) + .ok(); + } else { + set_hovered.set(entering); + } + }} }; - let mut listener_options = AddEventListenerOptions::new(); - listener_options.passive(true); + let listener_options = UseEventListenerOptions::default().passive(true); let _ = use_event_listener_with_options( el.clone(), mouseenter, move |_| toggle(true), - listener_options.clone(), + listener_options, ); let _ = diff --git a/src/use_element_size.rs b/src/use_element_size.rs index 64e4f3b..8831505 100644 --- a/src/use_element_size.rs +++ b/src/use_element_size.rs @@ -1,10 +1,14 @@ use crate::core::{ElementMaybeSignal, Size}; -use crate::{use_resize_observer_with_options, UseResizeObserverOptions}; -use crate::{watch_with_options, WatchOptions}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; 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. /// @@ -41,7 +45,7 @@ use wasm_bindgen::JsCast; /// /// ## 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 /// @@ -56,6 +60,7 @@ where } /// 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( target: El, options: UseElementSizeOptions, @@ -65,101 +70,104 @@ where El: Into>, T: Into + Clone + 'static, { - let window = window(); - let box_ = options.box_; - let initial_size = options.initial_size; + let UseElementSizeOptions { box_, initial_size } = options; - 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 = { - let target = target.clone(); + cfg_if! { if #[cfg(not(feature = "ssr"))] { - move || { - if let Some(target) = target.get_untracked() { - target - .into() - .namespace_uri() - .map(|ns| ns.contains("svg")) - .unwrap_or(false) - } else { - false + let box_ = box_.unwrap_or(web_sys::ResizeObserverBoxOptions::ContentBox); + + let target = target.into(); + + let is_svg = { + let target = target.clone(); + + move || { + if let Some(target) = target.get_untracked() { + target + .into() + .namespace_uri() + .map(|ns| ns.contains("svg")) + .unwrap_or(false) + } else { + false + } } - } - }; + }; - let (width, set_width) = create_signal(options.initial_size.width); - let (height, set_height) = create_signal(options.initial_size.height); + { + let target = target.clone(); - { - let target = target.clone(); + let _ = use_resize_observer_with_options::, _, _>( + target.clone(), + move |entries, _| { + let entry = &entries[0]; - let _ = use_resize_observer_with_options::, _, _>( - target.clone(), - move |entries, _| { - let entry = &entries[0]; - - 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(), - ); + let box_size = match box_ { + web_sys::ResizeObserverBoxOptions::ContentBox => entry.content_box_size(), + web_sys::ResizeObserverBoxOptions::BorderBox => entry.border_box_size(), + web_sys::ResizeObserverBoxOptions::DevicePixelContentBox => { + entry.device_pixel_content_box_size() } - } - } else if !box_size.is_null() && !box_size.is_undefined() && box_size.length() > 0 { - let format_box_size = if box_size.is_array() { - box_size.to_vec() - } else { - vec![box_size.into()] + _ => unreachable!(), }; - set_width.set(format_box_size.iter().fold(0.0, |acc, v| { - acc + v.as_ref().clone().unchecked_into::().inline_size() - })); - set_height.set(format_box_size.iter().fold(0.0, |acc, v| { - acc + v.as_ref().clone().unchecked_into::().block_size() - })) + 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(), + ); + } + } + } 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::().inline_size() + })); + set_height.set(format_box_size.iter().fold(0.0, |acc, v| { + acc + v.as_ref().clone().unchecked_into::().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 { - // fallback - set_width.set(entry.content_rect().width()); - set_height.set(entry.content_rect().height()) + set_width.set(0.0); + set_height.set(0.0); } }, - 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 { width: width.into(), @@ -175,24 +183,19 @@ pub struct UseElementSizeOptions { initial_size: Size, /// 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, } impl Default for UseElementSizeOptions { fn default() -> Self { Self { initial_size: Size::default(), - box_: web_sys::ResizeObserverBoxOptions::ContentBox, + box_: None, } } } -impl From for UseResizeObserverOptions { - fn from(options: UseElementSizeOptions) -> Self { - Self::default().box_(options.box_) - } -} - /// The return value of [`use_element_size`]. pub struct UseElementSizeReturn { /// The width of the element. diff --git a/src/use_element_visibility.rs b/src/use_element_visibility.rs index 6be8b51..23a9543 100644 --- a/src/use_element_visibility.rs +++ b/src/use_element_visibility.rs @@ -1,9 +1,12 @@ use crate::core::ElementMaybeSignal; -use crate::{use_intersection_observer_with_options, UseIntersectionObserverOptions}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; 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. /// /// ## Demo @@ -33,7 +36,7 @@ use std::marker::PhantomData; /// /// ## 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 /// @@ -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( target: El, options: UseElementVisibilityOptions, @@ -61,20 +66,22 @@ where { let (is_visible, set_visible) = create_signal(false); - use_intersection_observer_with_options( - target.into(), - move |entries, _| { - // In some circumstances Chrome passes a first (or only) entry which has a zero bounding client rect - // and returns `is_intersecting` erroneously as `false`. - if let Some(entry) = entries.into_iter().find(|entry| { - let rect = entry.bounding_client_rect(); - rect.width() > 0.0 || rect.height() > 0.0 - }) { - set_visible.set(entry.is_intersecting()); - } - }, - UseIntersectionObserverOptions::default().root(options.viewport), - ); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + use_intersection_observer_with_options( + target.into(), + move |entries, _| { + // In some circumstances Chrome passes a first (or only) entry which has a zero bounding client rect + // and returns `is_intersecting` erroneously as `false`. + if let Some(entry) = entries.into_iter().find(|entry| { + let rect = entry.bounding_client_rect(); + rect.width() > 0.0 || rect.height() > 0.0 + }) { + set_visible.set(entry.is_intersecting()); + } + }, + UseIntersectionObserverOptions::default().root(options.viewport), + ); + }} is_visible.into() } @@ -92,6 +99,7 @@ where /// 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. /// 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, #[builder(skip)] diff --git a/src/use_event_listener.rs b/src/use_event_listener.rs index 51f9d5c..e8c218e 100644 --- a/src/use_event_listener.rs +++ b/src/use_event_listener.rs @@ -1,11 +1,16 @@ 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::*; -use std::cell::RefCell; -use std::rc::Rc; -use wasm_bindgen::closure::Closure; -use wasm_bindgen::JsCast; + +cfg_if! { if #[cfg(not(feature = "ssr"))] { + use crate::{watch_with_options, WatchOptions}; + use leptos::*; + use std::cell::RefCell; + use std::rc::Rc; + use wasm_bindgen::closure::Closure; + use wasm_bindgen::JsCast; +}} /// Use EventListener with ease. /// 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::ev::visibilitychange; -/// # use leptos_use::use_event_listener; +/// # use leptos::logging::log; +/// # use leptos_use::{use_document, use_event_listener}; /// # /// # #[component] /// # fn Demo() -> impl IntoView { -/// use_event_listener(document(), visibilitychange, |evt| { +/// use_event_listener(use_document(), visibilitychange, |evt| { /// log!("{:?}", evt); /// }); /// # view! { } @@ -33,6 +39,7 @@ use wasm_bindgen::JsCast; /// ``` /// # use leptos::*; /// # use leptos::ev::click; +/// # use leptos::logging::log; /// # use leptos_use::use_event_listener; /// # /// # #[component] @@ -61,6 +68,7 @@ use wasm_bindgen::JsCast; /// ``` /// # use leptos::*; /// # use leptos::ev::keydown; +/// # use leptos::logging::log; /// # use web_sys::KeyboardEvent; /// # use leptos_use::use_event_listener; /// # @@ -78,7 +86,7 @@ use wasm_bindgen::JsCast; /// /// ## 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(target: El, event: Ev, handler: F) -> impl Fn() + Clone where Ev: EventDescriptor + 'static, @@ -86,20 +94,16 @@ where T: Into + Clone + 'static, F: FnMut(::EventType) + 'static, { - use_event_listener_with_options( - target, - event, - handler, - web_sys::AddEventListenerOptions::new(), - ) + use_event_listener_with_options(target, event, handler, UseEventListenerOptions::default()) } /// 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( target: El, event: Ev, handler: F, - options: web_sys::AddEventListenerOptions, + options: UseEventListenerOptions, ) -> impl Fn() + Clone where Ev: EventDescriptor + 'static, @@ -107,65 +111,122 @@ where T: Into + Clone + 'static, F: FnMut(::EventType) + 'static, { - let event_name = event.name(); - let closure_js = Closure::wrap(Box::new(handler) as Box).into_js_value(); + cfg_if! { if #[cfg(feature = "ssr")] { + || {} + } else { + let event_name = event.name(); + let closure_js = Closure::wrap(Box::new(handler) as Box).into_js_value(); - let cleanup_fn = { - let closure_js = closure_js.clone(); - let options = options.clone(); + let cleanup_fn = { + let closure_js = closure_js.clone(); + let options = options.as_add_event_listener_options(); - move |element: &web_sys::EventTarget| { - let _ = element.remove_event_listener_with_callback_and_event_listener_options( - &event_name, - closure_js.as_ref().unchecked_ref(), - options.unchecked_ref(), - ); - } - }; - - let event_name = event.name(); - - let signal = (target).into(); - - let prev_element = Rc::new(RefCell::new(None::)); - - let cleanup_prev_element = { - let prev_element = prev_element.clone(); - - move || { - if let Some(element) = prev_element.take() { - cleanup_fn(&element); + move |element: &web_sys::EventTarget| { + let _ = element.remove_event_listener_with_callback_and_event_listener_options( + &event_name, + closure_js.as_ref().unchecked_ref(), + options.unchecked_ref(), + ); } - } - }; + }; - let stop_watch = { - let cleanup_prev_element = cleanup_prev_element.clone(); + let event_name = event.name(); - watch_with_options( - move || signal.get().map(|e| e.into()), - move |element, _, _| { - cleanup_prev_element(); - prev_element.replace(element.clone()); + let signal = (target).into(); - if let Some(element) = element { - _ = element.add_event_listener_with_callback_and_add_event_listener_options( - &event_name, - closure_js.as_ref().unchecked_ref(), - &options, - ); + let prev_element = Rc::new(RefCell::new(None::)); + + let cleanup_prev_element = { + let prev_element = prev_element.clone(); + + move || { + if let Some(element) = prev_element.take() { + cleanup_fn(&element); } - }, - WatchOptions::default().immediate(true), - ) - }; + } + }; - let stop = move || { - stop_watch(); - cleanup_prev_element(); - }; + let stop_watch = { + let cleanup_prev_element = cleanup_prev_element.clone(); - 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, +} + +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 + } } diff --git a/src/use_geolocation.rs b/src/use_geolocation.rs new file mode 100644 index 0000000..034828f --- /dev/null +++ b/src/use_geolocation.rs @@ -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 { + 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 { + let (located_at, set_located_at) = create_signal(None::); + let (error, set_error) = create_signal(None::); + let (coords, set_coords) = create_signal(None::); + + 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::)); + + 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); + let on_error = + Closure::wrap(Box::new(on_error) as Box); + + 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 +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>, + + /// The timestamp of the current coordinates. + pub located_at: Signal>, + + /// The last error received from `navigator.geolocation`. + pub error: Signal>, + + /// Resume the geolocation watch. + pub resume: ResumeFn, + + /// Pause the geolocation watch. + pub pause: PauseFn, +} diff --git a/src/use_intersection_observer.rs b/src/use_intersection_observer.rs index fd462c3..fda59bf 100644 --- a/src/use_intersection_observer.rs +++ b/src/use_intersection_observer.rs @@ -1,11 +1,15 @@ use crate::core::{ElementMaybeSignal, ElementsMaybeSignal}; -use crate::{watch_with_options, WatchOptions}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; -use std::cell::RefCell; 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). /// @@ -44,7 +48,7 @@ use wasm_bindgen::prelude::*; /// /// ## 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 /// @@ -66,6 +70,7 @@ where } /// 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( target: El, mut callback: F, @@ -86,107 +91,113 @@ where .. } = options; - let closure_js = Closure::::new( - move |entries: js_sys::Array, observer| { - callback( - entries - .to_vec() - .into_iter() - .map(|v| v.unchecked_into::()) - .collect(), - observer, - ); - }, - ) - .into_js_value(); - let (is_active, set_active) = create_signal(immediate); - let observer: Rc>> = Rc::new(RefCell::new(None)); - - let cleanup = { - let obsserver = Rc::clone(&observer); - - move || { - if let Some(o) = obsserver.take() { - o.disconnect(); - } - } - }; - - let targets = (target).into(); - let root = root.map(|root| (root).into()); - - 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::(), + cfg_if! { if #[cfg(feature = "ssr")] { + let pause = || {}; + let cleanup = || {}; + let stop = || {}; + } else { + let closure_js = Closure::::new( + move |entries: js_sys::Array, observer| { + callback( + entries + .to_vec() + .into_iter() + .map(|v| v.unchecked_into::()) + .collect(), + observer, ); - - 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 cleanup = cleanup.clone(); + let observer: Rc>> = Rc::new(RefCell::new(None)); - move || { - cleanup(); - stop_watch(); - } - }; + let cleanup = { + let obsserver = Rc::clone(&observer); - on_cleanup(stop.clone()); + move || { + if let Some(o) = obsserver.take() { + o.disconnect(); + } + } + }; - let pause = { - let cleanup = cleanup.clone(); + let targets = (target).into(); + let root = root.map(|root| (root).into()); - move || { - cleanup(); - set_active.set(false); - } - }; + 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::(), + ); + + 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 { is_active: is_active.into(), diff --git a/src/use_mouse.rs b/src/use_mouse.rs index b76b84b..5e4e17b 100644 --- a/src/use_mouse.rs +++ b/src/use_mouse.rs @@ -1,14 +1,13 @@ #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] 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 default_struct_builder::DefaultBuilder; use leptos::ev::{dragover, mousemove, touchend, touchmove, touchstart}; use leptos::*; use std::marker::PhantomData; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::AddEventListenerOptions; /// Reactive mouse position /// @@ -158,40 +157,39 @@ where cfg_if! { if #[cfg(not(feature = "ssr"))] { let target = options.target; - let mut event_listener_options = AddEventListenerOptions::new(); - event_listener_options.passive(true); + let event_listener_options = UseEventListenerOptions::default().passive(true); let _ = use_event_listener_with_options( target.clone(), mousemove, mouse_handler, - event_listener_options.clone(), + event_listener_options, ); let _ = use_event_listener_with_options( target.clone(), dragover, drag_handler, - event_listener_options.clone(), + event_listener_options, ); if options.touch && !matches!(options.coord_type, UseMouseCoordType::Movement) { let _ = use_event_listener_with_options( target.clone(), touchstart, touch_handler.clone(), - event_listener_options.clone(), + event_listener_options, ); let _ = use_event_listener_with_options( target.clone(), touchmove, touch_handler, - event_listener_options.clone(), + event_listener_options, ); if options.reset_on_touch_ends { let _ = use_event_listener_with_options( target, touchend, move |_| reset(), - event_listener_options.clone(), + event_listener_options, ); } } diff --git a/src/use_mutation_observer.rs b/src/use_mutation_observer.rs index beb217f..f518000 100644 --- a/src/use_mutation_observer.rs +++ b/src/use_mutation_observer.rs @@ -1,10 +1,14 @@ use crate::core::ElementsMaybeSignal; -use crate::use_supported; +use cfg_if::cfg_if; +use default_struct_builder::DefaultBuilder; use leptos::*; -use std::cell::RefCell; -use std::rc::Rc; 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). /// @@ -19,16 +23,13 @@ use web_sys::MutationObserverInit; /// ``` /// # use leptos::*; /// # use leptos::html::Pre; -/// # use leptos_use::use_mutation_observer_with_options; +/// # use leptos_use::{use_mutation_observer_with_options, UseMutationObserverOptions}; /// # /// # #[component] /// # fn Demo() -> impl IntoView { /// let el = create_node_ref::
();
 /// let (text, set_text) = create_signal("".to_string());
 ///
-/// let mut init = web_sys::MutationObserverInit::new();
-/// init.attributes(true);
-///
 /// use_mutation_observer_with_options(
 ///     el,
 ///     move |mutations, _| {
@@ -36,7 +37,7 @@ use web_sys::MutationObserverInit;
 ///             set_text.update(|text| *text = format!("{text}\n{:?}", mutation.attribute_name()));
 ///         }
 ///     },
-///     init,
+///     UseMutationObserverOptions::default().attributes(true),
 /// );
 ///
 /// view! {
@@ -47,7 +48,7 @@ use web_sys::MutationObserverInit;
 ///
 /// ## 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(
     target: El,
     callback: F,
@@ -57,84 +58,167 @@ where
     T: Into + Clone + 'static,
     F: FnMut(Vec, 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(
     target: El,
     mut callback: F,
-    options: web_sys::MutationObserverInit,
+    options: UseMutationObserverOptions,
 ) -> UseMutationObserverReturn
 where
     El: Into>,
     T: Into + Clone + 'static,
     F: FnMut(Vec, web_sys::MutationObserver) + 'static,
 {
-    let closure_js = Closure::::new(
-        move |entries: js_sys::Array, observer| {
-            callback(
-                entries
-                    .to_vec()
-                    .into_iter()
-                    .map(|v| v.unchecked_into::())
-                    .collect(),
-                observer,
-            );
-        },
-    )
-    .into_js_value();
-
-    let observer: Rc>> = 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;
-            }
+    cfg_if! { if #[cfg(feature = "ssr")] {
+        UseMutationObserverReturn {
+            is_supported: Signal::derive(|| true),
+            stop: || {},
         }
-    };
-
-    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());
-                    }
-
-                    observer.replace(Some(obs));
-                }
+    } else {
+        let closure_js = Closure::::new(
+            move |entries: js_sys::Array, observer| {
+                callback(
+                    entries
+                        .to_vec()
+                        .into_iter()
+                        .map(|v| v.unchecked_into::())
+                        .collect(),
+                    observer,
+                );
             },
-            false,
         )
-    };
+        .into_js_value();
 
-    let stop = move || {
-        cleanup();
-        stop_watch();
-    };
+        let observer: Rc>> = Rc::new(RefCell::new(None));
 
-    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>,
+
+    /// 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,
+
+    /// 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 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`].
diff --git a/src/use_resize_observer.rs b/src/use_resize_observer.rs
index 9dfdaf3..1c61081 100644
--- a/src/use_resize_observer.rs
+++ b/src/use_resize_observer.rs
@@ -1,10 +1,14 @@
 use crate::core::ElementsMaybeSignal;
-use crate::use_supported;
+use cfg_if::cfg_if;
 use default_struct_builder::DefaultBuilder;
 use leptos::*;
-use std::cell::RefCell;
-use std::rc::Rc;
-use wasm_bindgen::prelude::*;
+
+cfg_if! { if #[cfg(not(feature = "ssr"))] {
+    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.
 ///
@@ -45,7 +49,7 @@ use wasm_bindgen::prelude::*;
 ///
 /// ## 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
 ///
@@ -63,6 +67,7 @@ where
 }
 
 /// 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(
     target: El, // TODO : multiple elements?
     mut callback: F,
@@ -73,92 +78,95 @@ where
     T: Into + Clone + 'static,
     F: FnMut(Vec, web_sys::ResizeObserver) + 'static,
 {
-    let closure_js = Closure::::new(
-        move |entries: js_sys::Array, observer| {
-            callback(
-                entries
-                    .to_vec()
-                    .into_iter()
-                    .map(|v| v.unchecked_into::())
-                    .collect(),
-                observer,
-            );
-        },
-    )
-    .into_js_value();
-
-    let observer: Rc>> = 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;
-            }
+    cfg_if! { if #[cfg(feature = "ssr")] {
+        UseResizeObserverReturn {
+            is_supported: Signal::derive(|| true),
+            stop: || {}
         }
-    };
-
-    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));
-                }
+    } else {
+        let closure_js = Closure::::new(
+            move |entries: js_sys::Array, observer| {
+                callback(
+                    entries
+                        .to_vec()
+                        .into_iter()
+                        .map(|v| v.unchecked_into::())
+                        .collect(),
+                    observer,
+                );
             },
-            false,
         )
-    };
+        .into_js_value();
 
-    let stop = move || {
-        cleanup();
-        stop_watch();
-    };
+        let observer: Rc>> = Rc::new(RefCell::new(None));
 
-    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`].
-#[derive(DefaultBuilder, Clone)]
+#[derive(DefaultBuilder, Clone, Default)]
 pub struct UseResizeObserverOptions {
     /// The box that is used to determine the dimensions of the target. Defaults to `ContentBox`.
-    pub box_: web_sys::ResizeObserverBoxOptions,
-}
-
-impl Default for UseResizeObserverOptions {
-    fn default() -> Self {
-        Self {
-            box_: web_sys::ResizeObserverBoxOptions::ContentBox,
-        }
-    }
+    #[builder(into)]
+    pub box_: Option,
 }
 
 impl From for web_sys::ResizeObserverOptions {
     fn from(val: UseResizeObserverOptions) -> Self {
         let mut options = web_sys::ResizeObserverOptions::new();
-        options.box_(val.box_);
+        options.box_(
+            val.box_
+                .unwrap_or(web_sys::ResizeObserverBoxOptions::ContentBox),
+        );
         options
     }
 }
diff --git a/src/use_scroll.rs b/src/use_scroll.rs
index 9913fb8..94c2c12 100644
--- a/src/use_scroll.rs
+++ b/src/use_scroll.rs
@@ -1,13 +1,25 @@
 use crate::core::ElementMaybeSignal;
-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 crate::UseEventListenerOptions;
 use cfg_if::cfg_if;
 use default_struct_builder::DefaultBuilder;
-use leptos::ev::scrollend;
 use leptos::*;
 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;
 
+/// 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.
 ///
 /// ## Demo
@@ -172,7 +184,7 @@ where
 }
 
 /// 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(element: El, options: UseScrollOptions) -> UseScrollReturn
 where
     El: Clone,
@@ -377,7 +389,7 @@ where
                 target,
                 ev::scroll,
                 handler,
-                options.event_listener_options.clone().unwrap_or_default(),
+                options.event_listener_options,
             );
         } else {
             let _ = use_event_listener_with_options::<
@@ -389,7 +401,7 @@ where
                 target,
                 ev::scroll,
                 on_scroll_handler,
-                options.event_listener_options.clone().unwrap_or_default(),
+                options.event_listener_options,
             );
         }
 
@@ -402,7 +414,7 @@ where
             target,
             scrollend,
             on_scroll_end,
-            options.event_listener_options.unwrap_or_default(),
+            options.event_listener_options,
         );
 
         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`].
 #[derive(DefaultBuilder)]
 /// Options for [`use_scroll_with_options`].
+#[cfg_attr(feature = "ssr", allow(dead_code))]
 pub struct UseScrollOptions {
     /// Throttle time in milliseconds for the scroll events. Defaults to 0 (disabled).
     throttle: f64,
@@ -453,8 +460,7 @@ pub struct UseScrollOptions {
     on_stop: Rc,
 
     /// Options passed to the `addEventListener("scroll", ...)` call
-    #[builder(into)]
-    event_listener_options: Option,
+    event_listener_options: UseEventListenerOptions,
 
     /// When changing the `x` or `y` signals this specifies the scroll behaviour.
     /// Can be `Auto` (= not smooth) or `Smooth`. Defaults to `Auto`.
diff --git a/src/use_window.rs b/src/use_window.rs
new file mode 100644
index 0000000..b9dae5a
--- /dev/null
+++ b/src/use_window.rs
@@ -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`.
+/// 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);
+
+impl Deref for UseWindow {
+    type Target = Option;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl UseWindow {
+    /// Returns the `Some(Navigator)` in the Browser. `None` otherwise.
+    pub fn navigator(&self) -> Option {
+        self.0.as_ref().map(|w| w.navigator())
+    }
+
+    /// Returns the same as [`use_document`].
+    #[inline(always)]
+    pub fn document(&self) -> UseDocument {
+        use_document()
+    }
+}
diff --git a/src/use_window_scroll.rs b/src/use_window_scroll.rs
index 8906a38..9b1fbd9 100644
--- a/src/use_window_scroll.rs
+++ b/src/use_window_scroll.rs
@@ -1,10 +1,9 @@
 #![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 leptos::ev::scroll;
 use leptos::*;
-use web_sys::AddEventListenerOptions;
 
 /// Reactive window scroll.
 ///
@@ -40,21 +39,17 @@ pub fn use_window_scroll() -> (Signal, Signal) {
     let (x, set_x) = create_signal(initial_x);
     let (y, set_y) = create_signal(initial_y);
 
-    cfg_if! { if #[cfg(not(feature = "ssr"))] {
-        let mut options = AddEventListenerOptions::new();
-        options.capture(false);
-        options.passive(true);
-
-        let _ = use_event_listener_with_options(
-                        window(),
-            scroll,
-            move |_| {
-                set_x.set(window().scroll_x().unwrap_or_default());
-                set_y.set(window().scroll_y().unwrap_or_default());
-            },
-            options,
-        );
-    }}
+    let _ = use_event_listener_with_options(
+        use_window(),
+        scroll,
+        move |_| {
+            set_x.set(window().scroll_x().unwrap_or_default());
+            set_y.set(window().scroll_y().unwrap_or_default());
+        },
+        UseEventListenerOptions::default()
+            .capture(false)
+            .passive(true),
+    );
 
     (x.into(), y.into())
 }
diff --git a/src/watch_debounced.rs b/src/watch_debounced.rs
index c23d0b5..55d6b26 100644
--- a/src/watch_debounced.rs
+++ b/src/watch_debounced.rs
@@ -12,6 +12,7 @@ use leptos::*;
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::watch_debounced;
 /// #
 /// # pub fn Demo() -> impl IntoView {
@@ -35,6 +36,7 @@ use leptos::*;
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::{watch_debounced_with_options, WatchDebouncedOptions};
 /// #
 /// # pub fn Demo() -> impl IntoView {
diff --git a/src/watch_pausable.rs b/src/watch_pausable.rs
index 7f0a7c7..62099e7 100644
--- a/src/watch_pausable.rs
+++ b/src/watch_pausable.rs
@@ -11,6 +11,7 @@ use leptos::*;
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::{watch_pausable, WatchPausableReturn};
 /// #
 /// # pub fn Demo() -> impl IntoView {
diff --git a/src/watch_throttled.rs b/src/watch_throttled.rs
index 535fb54..be45644 100644
--- a/src/watch_throttled.rs
+++ b/src/watch_throttled.rs
@@ -11,6 +11,7 @@ use default_struct_builder::DefaultBuilder;
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::watch_throttled;
 /// #
 /// # pub fn Demo() -> impl IntoView {
@@ -34,6 +35,7 @@ use default_struct_builder::DefaultBuilder;
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::{watch_throttled_with_options, WatchThrottledOptions};
 /// #
 /// # pub fn Demo() -> impl IntoView {
diff --git a/src/watch_with_options.rs b/src/watch_with_options.rs
index b0ab20d..c119732 100644
--- a/src/watch_with_options.rs
+++ b/src/watch_with_options.rs
@@ -17,6 +17,7 @@ use std::rc::Rc;
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::{watch_with_options, WatchOptions};
 /// #
 /// # pub fn Demo() -> impl IntoView {
@@ -41,6 +42,7 @@ use std::rc::Rc;
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::{watch_with_options, WatchOptions};
 /// #
 /// # pub fn Demo() -> impl IntoView {
@@ -59,6 +61,7 @@ use std::rc::Rc;
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::{watch_with_options, WatchOptions};
 /// #
 /// # pub fn Demo() -> impl IntoView {
diff --git a/src/whenever.rs b/src/whenever.rs
index 74c9bde..b8f4c3c 100644
--- a/src/whenever.rs
+++ b/src/whenever.rs
@@ -6,6 +6,7 @@ use crate::{watch_with_options, WatchOptions};
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::whenever;
 /// #
 /// # pub fn Demo() -> impl IntoView {
@@ -23,6 +24,7 @@ use crate::{watch_with_options, WatchOptions};
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::whenever;
 /// #
 /// # pub fn Demo() -> impl IntoView {
@@ -41,6 +43,7 @@ use crate::{watch_with_options, WatchOptions};
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::whenever;
 /// #
 /// # pub fn Demo() -> impl IntoView {
@@ -60,6 +63,7 @@ use crate::{watch_with_options, WatchOptions};
 ///
 /// ```
 /// # use leptos::*;
+/// # use leptos::logging::log;
 /// # use leptos_use::{WatchOptions, whenever_with_options};
 /// #
 /// # pub fn Demo() -> impl IntoView {