added better demo integration and styling to book

This commit is contained in:
Maccesch 2023-05-26 18:09:01 +01:00
parent 6a9a0b5927
commit 5057bf0765
31 changed files with 610 additions and 775 deletions

5
CHANGELOG.md Normal file
View file

@ -0,0 +1,5 @@
# Changelog
## 0.1.3
- Added `use_scroll`.

View file

@ -14,6 +14,6 @@ repository = "https://github.com/Synphonyte/leptos-use"
[dependencies] [dependencies]
leptos = "0.3" leptos = "0.3"
web-sys = "0.3" web-sys = { version = "0.3", features = ["ScrollToOptions", "ScrollBehavior"] }
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
js-sys = "0.3" js-sys = "0.3"

View file

@ -7,7 +7,6 @@ title = "Leptos-Use Documentation"
[output.html] [output.html]
no-section-label = true no-section-label = true
additional-css = ["custom.css"] additional-css = ["src/custom.css", "src/demo.css"]
additional-js = ["demo-iframe.js"]
[preprocessor.cmdrun] [preprocessor.cmdrun]

View file

@ -1,15 +0,0 @@
iframe.demo {
transition: height 0.25s ease-out;
}
.demo-container {
position: relative;
}
.demo-container > a.demo-source {
position: absolute;
right: 10px;
top: 5px;
font-size: 80%;
color: rgb(96 165 250);
}

View file

@ -1,19 +0,0 @@
const iframes = Array.prototype.slice.apply(document.getElementsByTagName("iframe"));
for (const [i, iframe] of iframes.entries()) {
iframe.style.height = iframe.getBoundingClientRect().height + "px";
iframe.addEventListener('load', () => {
const innerBody = window.frames[i].document.body;
innerBody.style.overflow = "hidden";
const resize = () => {
if (innerBody.scrollHeight == 0) {
window.setTimeout(resize, 50);
return;
}
iframe.style.height = innerBody.scrollHeight + "px";
}
window.setTimeout(resize, 50);
});
}

View file

@ -29,6 +29,32 @@ def build_and_copy_demo(category, md_name):
shutil.copytree(example_output_path, target_path, shutil.copytree(example_output_path, target_path,
dirs_exist_ok=True) dirs_exist_ok=True)
with open(os.path.join(target_path, "index.html"), "r") as f:
html = f.read().replace("./demo", f"./{name}/demo")
head = html.split("<head>")[1].split("</head>")[0]
body = html.split("<body>")[1].split("</body>")[0]
book_html_path = os.path.join("book", category, f"{name}.html")
with open(book_html_path, "r") as f:
html = f.read()
head_split = html.split("<head>")
target_head = head_split[1].split("</head>")[0]
body_split = html.split("<body>")[1].split("</body>")
target_body = body_split[0]
with open(book_html_path, "w") as f:
f.write(
f"""{head_split[0]}
<head>
{head}
{target_head}
</head>
<body>
{body}
{target_body}
</body>
{body_split[1]}""")
if __name__ == '__main__': if __name__ == '__main__':
main() main()

8
docs/book/src/custom.css Normal file
View file

@ -0,0 +1,8 @@
.light {
--fg: #333;
}
pre > code {
border-radius: 5px;
padding: 1.5rem;
}

66
docs/book/src/demo.css Normal file
View file

@ -0,0 +1,66 @@
:root {
--brand-color: #EF3939;
--brand-color-dark: #9c2525;
}
.demo-container {
position: relative;
}
.demo-container > a.demo-source {
position: absolute;
right: 10px;
top: 8px;
font-size: 12px;
font-weight: 500;
}
.demo-container > a.demo-source > i.fa {
font-size: 20px;
vertical-align: middle;
margin-left: 5px;
}
.demo-container {
border-radius: 5px;
padding: 1.5rem;
background-color: #f6f7f6;
}
.ayu .demo-container, .navy .demo-container, .coal .demo-container {
background-color: #1d1f21;
}
.demo-container button {
background-color: var(--brand-color);
font-family: inherit;
padding: 3px 15px;
border: none;
outline: none;
color: white;
margin: 1rem 0;
border-bottom: 2px solid var(--brand-color-dark);
text-shadow: 1px 1px 1px var(--brand-color-dark);
border-radius: 4px;
font-size: inherit;
box-sizing: border-box;
vertical-align: middle;
}
.demo-container button:hover {
background-color: var(--brand-color-dark);
}
.demo-container button:active {
border-bottom: 0;
border-top: 2px solid var(--brand-color-dark);
}
.demo-container p {
margin: 1rem 0;
}
.demo-container .note {
opacity: 0.7;
font-size: 1.4rem;
}

View file

@ -34,9 +34,8 @@ def process_line(line, name):
if stripped.startswith("[Link to Demo](https://"): if stripped.startswith("[Link to Demo](https://"):
example_link = stripped.replace("[Link to Demo](", "").replace(")", "") example_link = stripped.replace("[Link to Demo](", "").replace(")", "")
result = f'''<div class="demo-container"> result = f'''<div class="demo-container">
<a class="demo-source" href="{example_link}/src/main.rs" target="_blank">source</a> <a class="demo-source" href="{example_link}/src/main.rs" target="_blank">source <i class="fa fa-github"></i></a>
<iframe class="demo" src="{name}/demo/index.html" width="100%" frameborder="0"> <div id="demo-anchor"></div>
</iframe>
</div>''' </div>'''
else: else:

View file

@ -1,3 +1,7 @@
# Functions # Functions
<!-- cmdrun python3 generate_function_overview.py browser --> <!-- cmdrun python3 generate_function_overview.py browser -->
<!-- cmdrun python3 generate_function_overview.py sensors -->
<!-- cmdrun python3 generate_function_overview.py utilities -->

View file

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

View file

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

View file

@ -1,2 +1,2 @@
[build] [build]
public_url = "/leptos-use/utilities/use_throttle_fn/demo/" public_url = "./demo/"

View file

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

View file

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

View file

@ -1,5 +1,6 @@
use leptos::*; use leptos::*;
use leptos_use::use_throttle_fn; use leptos_use::use_throttle_fn;
use leptos_use::utils::demo_or_body;
#[component] #[component]
fn Demo(cx: Scope) -> impl IntoView { fn Demo(cx: Scope) -> impl IntoView {
@ -15,12 +16,11 @@ fn Demo(cx: Scope) -> impl IntoView {
set_click_count(click_count() + 1); set_click_count(click_count() + 1);
throttled_fn(); throttled_fn();
} }
class="rounded bg-blue-500 hover:bg-blue-400 py-2 px-4 text-white"
> >
"Smash me!" "Smash me!"
</button> </button>
<p class="my-2"><small class="block">"Delay is set to 1000ms for this demo."</small></p> <div class="note">"Delay is set to 1000ms for this demo."</div>
<p class="my-3">"Button clicked: " { click_count }</p> <p>"Button clicked: " { click_count }</p>
<p>"Event handler called: " { throttled_count }</p> <p>"Event handler called: " { throttled_count }</p>
} }
} }
@ -29,11 +29,7 @@ fn main() {
_ = console_log::init_with_level(log::Level::Debug); _ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
mount_to_body(|cx| { mount_to(demo_or_body(), |cx| {
view! {cx, view! { cx, <Demo /> }
<div class="p-6 bg-gray-700 text-gray-300">
<Demo />
</div>
}
}) })
} }

View file

@ -1,574 +0,0 @@
/*
! tailwindcss v3.3.1 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::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-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-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: ;
}
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.block {
display: block;
}
.rounded {
border-radius: 0.25rem;
}
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.bg-gray-700 {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.p-6 {
padding: 1.5rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:bg-blue-400:hover {
--tw-bg-opacity: 1;
background-color: rgb(96 165 250 / var(--tw-bg-opacity));
}

View file

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

View file

@ -1,51 +1,55 @@
use leptos::html::ElementDescriptor; use leptos::html::ElementDescriptor;
use leptos::*; use leptos::*;
use std::marker::PhantomData;
use std::ops::Deref; use std::ops::Deref;
/// Used as an argument type to make it easily possible to pass either /// Used as an argument type to make it easily possible to pass either
/// * a `web_sys` element that implements `EventTarget`, /// * a `web_sys` element that implements `E` (for example `EventTarget` or `Element`),
/// * an `Option<T>` where `T` is the web_sys element, /// * an `Option<T>` where `T` is the web_sys element,
/// * a `Signal<T>` where `T` is the web_sys element, /// * a `Signal<T>` where `T` is the web_sys element,
/// * a `Signal<Option<T>>` where `T` is the web_sys element, /// * a `Signal<Option<T>>` where `T` is the web_sys element,
/// * a `NodeRef` /// * a `NodeRef`
/// into a function. Used for example in [`use_event_listener`]. /// into a function. Used for example in [`use_event_listener`].
pub enum EventTargetMaybeSignal<T> pub enum ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
Static(Option<T>), Static(Option<T>),
Dynamic(Signal<Option<T>>), Dynamic(Signal<Option<T>>),
_Phantom(PhantomData<E>),
} }
impl<T> Default for EventTargetMaybeSignal<T> impl<T, E> Default for ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
fn default() -> Self { fn default() -> Self {
Self::Static(None) Self::Static(None)
} }
} }
impl<T> Clone for EventTargetMaybeSignal<T> impl<T, E> Clone for ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
match self { match self {
Self::Static(t) => Self::Static(t.clone()), Self::Static(t) => Self::Static(t.clone()),
Self::Dynamic(s) => Self::Dynamic(*s), Self::Dynamic(s) => Self::Dynamic(*s),
_ => unreachable!(),
} }
} }
} }
impl<T> SignalGet<Option<T>> for EventTargetMaybeSignal<T> impl<T, E> SignalGet<Option<T>> for ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
fn get(&self) -> Option<T> { fn get(&self) -> Option<T> {
match self { match self {
Self::Static(t) => t.clone(), Self::Static(t) => t.clone(),
Self::Dynamic(s) => s.get(), Self::Dynamic(s) => s.get(),
_ => unreachable!(),
} }
} }
@ -53,18 +57,20 @@ where
match self { match self {
Self::Static(t) => Some(t.clone()), Self::Static(t) => Some(t.clone()),
Self::Dynamic(s) => s.try_get(), Self::Dynamic(s) => s.try_get(),
_ => unreachable!(),
} }
} }
} }
impl<T> SignalWith<Option<T>> for EventTargetMaybeSignal<T> impl<T, E> SignalWith<Option<T>> for ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
fn with<O>(&self, f: impl FnOnce(&Option<T>) -> O) -> O { fn with<O>(&self, f: impl FnOnce(&Option<T>) -> O) -> O {
match self { match self {
Self::Static(t) => f(t), Self::Static(t) => f(t),
Self::Dynamic(s) => s.with(f), Self::Dynamic(s) => s.with(f),
_ => unreachable!(),
} }
} }
@ -72,18 +78,20 @@ where
match self { match self {
Self::Static(t) => Some(f(t)), Self::Static(t) => Some(f(t)),
Self::Dynamic(s) => s.try_with(f), Self::Dynamic(s) => s.try_with(f),
_ => unreachable!(),
} }
} }
} }
impl<T> SignalWithUntracked<Option<T>> for EventTargetMaybeSignal<T> impl<T, E> SignalWithUntracked<Option<T>> for ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
fn with_untracked<O>(&self, f: impl FnOnce(&Option<T>) -> O) -> O { fn with_untracked<O>(&self, f: impl FnOnce(&Option<T>) -> O) -> O {
match self { match self {
Self::Static(t) => f(t), Self::Static(t) => f(t),
Self::Dynamic(s) => s.with_untracked(f), Self::Dynamic(s) => s.with_untracked(f),
_ => unreachable!(),
} }
} }
@ -91,18 +99,20 @@ where
match self { match self {
Self::Static(t) => Some(f(t)), Self::Static(t) => Some(f(t)),
Self::Dynamic(s) => s.try_with_untracked(f), Self::Dynamic(s) => s.try_with_untracked(f),
_ => unreachable!(),
} }
} }
} }
impl<T> SignalGetUntracked<Option<T>> for EventTargetMaybeSignal<T> impl<T, E> SignalGetUntracked<Option<T>> for ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
fn get_untracked(&self) -> Option<T> { fn get_untracked(&self) -> Option<T> {
match self { match self {
Self::Static(t) => t.clone(), Self::Static(t) => t.clone(),
Self::Dynamic(s) => s.get_untracked(), Self::Dynamic(s) => s.get_untracked(),
_ => unreachable!(),
} }
} }
@ -110,36 +120,37 @@ where
match self { match self {
Self::Static(t) => Some(t.clone()), Self::Static(t) => Some(t.clone()),
Self::Dynamic(s) => s.try_get_untracked(), Self::Dynamic(s) => s.try_get_untracked(),
_ => unreachable!(),
} }
} }
} }
impl<T> From<(Scope, T)> for EventTargetMaybeSignal<T> impl<T, E> From<(Scope, T)> for ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
fn from(value: (Scope, T)) -> Self { fn from(value: (Scope, T)) -> Self {
EventTargetMaybeSignal::Static(Some(value.1)) ElementMaybeSignal::Static(Some(value.1))
} }
} }
impl<T> From<(Scope, Option<T>)> for EventTargetMaybeSignal<T> impl<T, E> From<(Scope, Option<T>)> for ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
fn from(target: (Scope, Option<T>)) -> Self { fn from(target: (Scope, Option<T>)) -> Self {
EventTargetMaybeSignal::Static(target.1) ElementMaybeSignal::Static(target.1)
} }
} }
macro_rules! impl_from_signal_option { macro_rules! impl_from_signal_option {
($ty:ty) => { ($ty:ty) => {
impl<T> From<(Scope, $ty)> for EventTargetMaybeSignal<T> impl<T, E> From<(Scope, $ty)> for ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
fn from(target: (Scope, $ty)) -> Self { fn from(target: (Scope, $ty)) -> Self {
EventTargetMaybeSignal::Dynamic(target.1.into()) ElementMaybeSignal::Dynamic(target.1.into())
} }
} }
}; };
@ -152,14 +163,14 @@ impl_from_signal_option!(Memo<Option<T>>);
macro_rules! impl_from_signal { macro_rules! impl_from_signal {
($ty:ty) => { ($ty:ty) => {
impl<T> From<(Scope, $ty)> for EventTargetMaybeSignal<T> impl<T, E> From<(Scope, $ty)> for ElementMaybeSignal<T, E>
where where
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<E> + Clone + 'static,
{ {
fn from(target: (Scope, $ty)) -> Self { fn from(target: (Scope, $ty)) -> Self {
let (cx, signal) = target; let (cx, signal) = target;
EventTargetMaybeSignal::Dynamic(Signal::derive(cx, move || Some(signal.get()))) ElementMaybeSignal::Dynamic(Signal::derive(cx, move || Some(signal.get())))
} }
} }
}; };
@ -170,19 +181,26 @@ impl_from_signal!(ReadSignal<T>);
impl_from_signal!(RwSignal<T>); impl_from_signal!(RwSignal<T>);
impl_from_signal!(Memo<T>); impl_from_signal!(Memo<T>);
impl<R> From<(Scope, NodeRef<R>)> for EventTargetMaybeSignal<web_sys::EventTarget> macro_rules! impl_from_node_ref {
where ($ty:ty) => {
R: ElementDescriptor + Clone + 'static, impl<R> From<(Scope, NodeRef<R>)> for ElementMaybeSignal<$ty, $ty>
{ where
fn from(target: (Scope, NodeRef<R>)) -> Self { R: ElementDescriptor + Clone + 'static,
let (cx, node_ref) = target; {
fn from(target: (Scope, NodeRef<R>)) -> Self {
let (cx, node_ref) = target;
EventTargetMaybeSignal::Dynamic(Signal::derive(cx, move || { ElementMaybeSignal::Dynamic(Signal::derive(cx, move || {
node_ref.get().map(move |el| { node_ref.get().map(move |el| {
let el = el.into_any(); let el = el.into_any();
let el: web_sys::EventTarget = el.deref().clone().into(); let el: $ty = el.deref().clone().into();
el el
}) })
})) }))
} }
}
};
} }
impl_from_node_ref!(web_sys::EventTarget);
impl_from_node_ref!(web_sys::Element);

View file

@ -1,9 +1,11 @@
pub mod core; pub mod core;
pub mod use_debounce_fn;
pub mod use_event_listener; pub mod use_event_listener;
pub mod use_scroll; pub mod use_scroll;
pub mod use_throttle_fn; pub mod use_throttle_fn;
pub mod utils; pub mod utils;
pub use use_debounce_fn::*;
pub use use_event_listener::use_event_listener; pub use use_event_listener::use_event_listener;
pub use use_scroll::*; pub use use_scroll::*;
pub use use_throttle_fn::*; pub use use_throttle_fn::*;

42
src/use_debounce_fn.rs Normal file
View file

@ -0,0 +1,42 @@
use crate::utils::{
create_filter_wrapper, create_filter_wrapper_with_arg, debounce_filter, DebounceOptions,
};
use leptos::MaybeSignal;
pub fn use_debounce_fn<F>(func: F, ms: impl Into<MaybeSignal<f64>>) -> impl FnMut()
where
F: FnOnce() + Clone + 'static,
{
use_debounce_fn_with_options(func, ms, Default::default())
}
pub fn use_debounce_fn_with_options<F>(
func: F,
ms: impl Into<MaybeSignal<f64>>,
options: DebounceOptions,
) -> impl FnMut()
where
F: FnOnce() + Clone + 'static,
{
create_filter_wrapper(debounce_filter(ms, options), func)
}
pub fn use_debounce_fn_with_arg<F, Arg>(func: F, ms: impl Into<MaybeSignal<f64>>) -> impl FnMut(Arg)
where
F: FnOnce(Arg) + Clone + 'static,
Arg: Clone + 'static,
{
use_debounce_fn_with_arg_and_options(func, ms, Default::default())
}
pub fn use_debounce_fn_with_arg_and_options<F, Arg>(
func: F,
ms: impl Into<MaybeSignal<f64>>,
options: DebounceOptions,
) -> impl FnMut(Arg)
where
F: FnOnce(Arg) + Clone + 'static,
Arg: Clone + 'static,
{
create_filter_wrapper_with_arg(debounce_filter(ms, options), func)
}

View file

@ -1,4 +1,4 @@
use crate::core::EventTargetMaybeSignal; use crate::core::ElementMaybeSignal;
use leptos::ev::EventDescriptor; use leptos::ev::EventDescriptor;
use leptos::*; use leptos::*;
use std::cell::RefCell; use std::cell::RefCell;
@ -90,7 +90,7 @@ pub fn use_event_listener<Ev, El, T, F>(
) -> Box<dyn Fn()> ) -> Box<dyn Fn()>
where where
Ev: EventDescriptor + 'static, Ev: EventDescriptor + 'static,
(Scope, El): Into<EventTargetMaybeSignal<T>>, (Scope, El): Into<ElementMaybeSignal<T, web_sys::EventTarget>>,
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<web_sys::EventTarget> + Clone + 'static,
F: FnMut(<Ev as EventDescriptor>::EventType) + 'static, F: FnMut(<Ev as EventDescriptor>::EventType) + 'static,
{ {

View file

@ -1,34 +1,69 @@
use crate::core::EventTargetMaybeSignal; use crate::core::ElementMaybeSignal;
use crate::use_debounce_fn_with_arg;
use crate::utils::{CloneableFn, CloneableFnWithArg, CloneableFnWithReturn};
use leptos::*; use leptos::*;
/// 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;
#[allow(unused_variables)] #[allow(unused_variables)]
pub fn use_scroll<El, T, Fx, Fy>( pub fn use_scroll_with_options<El, T>(
cx: Scope, cx: Scope,
element: El, element: El,
options: UseScrollOptions, options: UseScrollOptions,
) -> UseScrollReturn ) -> UseScrollReturn
where where
(Scope, El): Into<EventTargetMaybeSignal<T>>, (Scope, El): Into<ElementMaybeSignal<T, web_sys::Element>>,
T: Into<web_sys::EventTarget> + Clone + 'static, T: Into<web_sys::Element> + Clone + 'static,
Fx: Fn(f64),
Fy: Fn(f64),
{ {
// TODO : implement // TODO : implement
let (x, set_x) = create_signal(cx, 0.0); let (internal_x, set_internal_x) = create_signal(cx, 0.0);
let (y, set_y) = create_signal(cx, 0.0); let (internal_y, set_internal_y) = create_signal(cx, 0.0);
let (is_scrolling, _) = create_signal(cx, false); let signal = (cx, element).into();
let (arrived_state, _) = create_signal( let behavior = options.behavior;
cx,
Directions { let scroll_to = move |x: Option<f64>, y: Option<f64>| {
left: false, let element = signal.get_untracked();
right: false,
top: false, if let Some(element) = element {
bottom: false, let element = element.into();
},
); let mut scroll_options = web_sys::ScrollToOptions::new();
let (directions, _) = create_signal( scroll_options.behavior(behavior.into());
if let Some(x) = x {
scroll_options.left(x);
}
if let Some(y) = y {
scroll_options.top(y);
}
element.scroll_to_with_scroll_to_options(&scroll_options);
}
};
let scroll = scroll_to.clone();
let set_x = Box::new(move |x| scroll(Some(x), None));
let scroll = scroll_to.clone();
let set_y = Box::new(move |y| scroll(None, Some(y)));
let (is_scrolling, set_is_scrolling) = create_signal(cx, false);
let (arrived_state, set_arrived_state) = create_signal(
cx,
Directions {
left: true,
right: false,
top: true,
bottom: false,
},
);
let (directions, set_directions) = create_signal(
cx, cx,
Directions { Directions {
left: false, left: false,
@ -38,11 +73,29 @@ where
}, },
); );
let on_stop = options.on_stop;
let on_scroll_end = move |e| {
if !is_scrolling.get() {
return;
}
set_is_scrolling(false);
set_directions.update(|directions| {
directions.left = false;
directions.right = false;
directions.top = false;
directions.bottom = false;
on_stop(e);
});
};
let on_scroll_end_debounced =
use_debounce_fn_with_arg(on_scroll_end, options.throttle + options.idle);
UseScrollReturn { UseScrollReturn {
x: x.into(), x: internal_x.into(),
set_x: Box::new(move |x| set_x.set(x)), set_x,
y: y.into(), y: internal_y.into(),
set_y: Box::new(move |y| set_y.set(y)), set_y,
is_scrolling: is_scrolling.into(), is_scrolling: is_scrolling.into(),
arrived_state: arrived_state.into(), arrived_state: arrived_state.into(),
directions: directions.into(), directions: directions.into(),
@ -50,39 +103,61 @@ where
} }
/// Options for [`use_scroll`]. /// Options for [`use_scroll`].
#[derive(Default)]
pub struct UseScrollOptions { pub struct UseScrollOptions {
/// Throttle time in milliseconds for the scroll events. Defaults to 0 (disabled). /// Throttle time in milliseconds for the scroll events. Defaults to 0 (disabled).
pub throttle: u32, pub throttle: f64,
/// After scrolling ends we wait idle + throttle milliseconds before we consider scrolling to have stopped. /// After scrolling ends we wait idle + throttle milliseconds before we consider scrolling to have stopped.
/// Defaults to 200. /// Defaults to 200.
pub idle: u32, pub idle: f64,
/// Threshold in pixels when we consider a side to have arrived (`UseScrollReturn::arrived_state`). /// Threshold in pixels when we consider a side to have arrived (`UseScrollReturn::arrived_state`).
pub offset: ScrollOffset, pub offset: ScrollOffset,
/// Callback when scrolling is happening. /// Callback when scrolling is happening.
pub on_scroll: Option<Box<dyn Fn()>>, pub on_scroll: Box<dyn CloneableFn>,
/// Callback when scrolling stops (after `idle` + `throttle` milliseconds have passed). /// Callback when scrolling stops (after `idle` + `throttle` milliseconds have passed).
pub on_stop: Option<Box<dyn Fn()>>, pub on_stop: Box<dyn CloneableFnWithArg<web_sys::Event>>,
/// Options passed to the `addEventListener("scroll", ...)` call /// Options passed to the `addEventListener("scroll", ...)` call
pub event_listener_options: Option<web_sys::AddEventListenerOptions>, pub event_listener_options: web_sys::AddEventListenerOptions,
/// When changing the `x` or `y` signals this specifies the scroll behaviour. /// When changing the `x` or `y` signals this specifies the scroll behaviour.
/// Can be `Auto` (= not smooth) or `Smooth`. Defaults to `Auto`. /// Can be `Auto` (= not smooth) or `Smooth`. Defaults to `Auto`.
pub behavior: ScrollBehavior, pub behavior: ScrollBehavior,
} }
#[derive(Default)] impl Default for UseScrollOptions {
fn default() -> Self {
Self {
throttle: 0.0,
idle: 200.0,
offset: Default::default(),
on_scroll: Default::default(),
on_stop: Default::default(),
event_listener_options: Default::default(),
behavior: Default::default(),
}
}
}
#[derive(Default, Copy, Clone)]
pub enum ScrollBehavior { pub enum ScrollBehavior {
#[default] #[default]
Auto, Auto,
Smooth, Smooth,
} }
impl Into<web_sys::ScrollBehavior> for ScrollBehavior {
fn into(self) -> web_sys::ScrollBehavior {
match self {
ScrollBehavior::Auto => web_sys::ScrollBehavior::Auto,
ScrollBehavior::Smooth => web_sys::ScrollBehavior::Smooth,
}
}
}
pub struct UseScrollReturn { pub struct UseScrollReturn {
pub x: Signal<f64>, pub x: Signal<f64>,
pub set_x: Box<dyn Fn(f64)>, pub set_x: Box<dyn Fn(f64)>,
@ -101,7 +176,7 @@ pub struct Directions {
pub bottom: bool, pub bottom: bool,
} }
#[derive(Default)] #[derive(Default, Copy, Clone)]
pub struct ScrollOffset { pub struct ScrollOffset {
pub left: f64, pub left: f64,
pub top: f64, pub top: f64,

View file

@ -1,4 +1,6 @@
use crate::utils::{create_filter_wrapper, create_filter_wrapper_with_arg, throttle_filter}; use crate::utils::{
create_filter_wrapper_with_return, create_filter_wrapper_with_return_and_arg, throttle_filter,
};
use leptos::MaybeSignal; use leptos::MaybeSignal;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
@ -41,10 +43,9 @@ pub use crate::utils::ThrottleOptions;
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos_use::{ThrottleOptions, use_throttle_fn_with_options}; /// # use leptos_use::{ThrottleOptions, use_throttle_fn_with_options};
///
/// # #[component] /// # #[component]
/// # fn Demo(cx: Scope) -> impl IntoView { /// # fn Demo(cx: Scope) -> impl IntoView {
/// let throttled_fn = use_throttle_fn_with_options( /// let throttled_fn = use_throttle_fn_with_options(
/// || { /// || {
/// // do something, it will be called at most 1 time per second /// // do something, it will be called at most 1 time per second
/// }, /// },
@ -58,7 +59,7 @@ pub use crate::utils::ThrottleOptions;
/// # } /// # }
/// ``` /// ```
/// ///
/// If your function that you want to throttle takes an argument there are also the versions /// If you want to throttle a function that takes an argument there are also the versions
/// [`use_throttle_fn_with_args`] and [`use_throttle_fn_with_args_and_options`]. /// [`use_throttle_fn_with_args`] and [`use_throttle_fn_with_args_and_options`].
/// ///
/// ## Recommended Reading /// ## Recommended Reading
@ -69,7 +70,7 @@ pub fn use_throttle_fn<F, R>(
ms: impl Into<MaybeSignal<f64>>, ms: impl Into<MaybeSignal<f64>>,
) -> impl FnMut() -> Rc<RefCell<Option<R>>> ) -> impl FnMut() -> Rc<RefCell<Option<R>>>
where where
F: FnMut() -> R + Clone + 'static, F: FnOnce() -> R + Clone + 'static,
R: 'static, R: 'static,
{ {
use_throttle_fn_with_options(func, ms, Default::default()) use_throttle_fn_with_options(func, ms, Default::default())
@ -82,10 +83,10 @@ pub fn use_throttle_fn_with_options<F, R>(
options: ThrottleOptions, options: ThrottleOptions,
) -> impl FnMut() -> Rc<RefCell<Option<R>>> ) -> impl FnMut() -> Rc<RefCell<Option<R>>>
where where
F: FnMut() -> R + Clone + 'static, F: FnOnce() -> R + Clone + 'static,
R: 'static, R: 'static,
{ {
create_filter_wrapper(throttle_filter(ms, options), func) create_filter_wrapper_with_return(throttle_filter(ms, options), func)
} }
/// Version of [`use_throttle_fn`] with an argument for the throttled function. See the docs for [`use_throttle_fn`] for how to use. /// Version of [`use_throttle_fn`] with an argument for the throttled function. See the docs for [`use_throttle_fn`] for how to use.
@ -94,8 +95,8 @@ pub fn use_throttle_fn_with_arg<F, Arg, R>(
ms: impl Into<MaybeSignal<f64>>, ms: impl Into<MaybeSignal<f64>>,
) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>> ) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>>
where where
F: FnMut(Arg) -> R + Clone + 'static, F: FnOnce(Arg) -> R + Clone + 'static,
Arg: 'static, Arg: Clone + 'static,
R: 'static, R: 'static,
{ {
use_throttle_fn_with_arg_and_options(func, ms, Default::default()) use_throttle_fn_with_arg_and_options(func, ms, Default::default())
@ -108,9 +109,9 @@ pub fn use_throttle_fn_with_arg_and_options<F, Arg, R>(
options: ThrottleOptions, options: ThrottleOptions,
) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>> ) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>>
where where
F: FnMut(Arg) -> R + Clone + 'static, F: FnOnce(Arg) -> R + Clone + 'static,
Arg: 'static, Arg: Clone + 'static,
R: 'static, R: 'static,
{ {
create_filter_wrapper_with_arg(throttle_filter(ms, options), func) create_filter_wrapper_with_return_and_arg(throttle_filter(ms, options), func)
} }

101
src/utils/clonable_fn.rs Normal file
View file

@ -0,0 +1,101 @@
pub trait CloneableFnWithReturn<R>: FnOnce() -> R {
fn clone_box(&self) -> Box<dyn CloneableFnWithReturn<R>>;
}
impl<F, R> CloneableFnWithReturn<R> for F
where
F: FnOnce() -> R + Clone + 'static,
R: 'static,
{
fn clone_box(&self) -> Box<dyn CloneableFnWithReturn<R>> {
Box::new(self.clone())
}
}
impl<R> Clone for Box<dyn CloneableFnWithReturn<R>> {
fn clone(&self) -> Self {
(**self).clone_box()
}
}
impl<R: Default + 'static> Default for Box<dyn CloneableFnWithReturn<R>> {
fn default() -> Self {
Box::new(|| Default::default())
}
}
pub trait CloneableFnWithReturnAndArg<R, Arg>: FnOnce(Arg) -> R {
fn clone_box(&self) -> Box<dyn CloneableFnWithReturnAndArg<R, Arg>>;
}
impl<F, R, Arg> CloneableFnWithReturnAndArg<R, Arg> for F
where
F: FnMut(Arg) -> R + Clone + 'static,
R: 'static,
{
fn clone_box(&self) -> Box<dyn CloneableFnWithReturnAndArg<R, Arg>> {
Box::new(self.clone())
}
}
impl<R, Arg> Clone for Box<dyn CloneableFnWithReturnAndArg<R, Arg>> {
fn clone(&self) -> Self {
(**self).clone_box()
}
}
impl<R: Default + 'static, Arg> Default for Box<dyn CloneableFnWithReturnAndArg<R, Arg>> {
fn default() -> Self {
Box::new(|_| Default::default())
}
}
pub trait CloneableFn: FnOnce() {
fn clone_box(&self) -> Box<dyn CloneableFn>;
}
impl<F> CloneableFn for F
where
F: FnOnce() + Clone + 'static,
{
fn clone_box(&self) -> Box<dyn CloneableFn> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn CloneableFn> {
fn clone(&self) -> Self {
(**self).clone_box()
}
}
impl Default for Box<dyn CloneableFn> {
fn default() -> Self {
Box::new(|| {})
}
}
pub trait CloneableFnWithArg<Arg>: FnOnce(Arg) {
fn clone_box(&self) -> Box<dyn CloneableFnWithArg<Arg>>;
}
impl<F, Arg> CloneableFnWithArg<Arg> for F
where
F: FnMut(Arg) + Clone + 'static,
{
fn clone_box(&self) -> Box<dyn CloneableFnWithArg<Arg>> {
Box::new(self.clone())
}
}
impl<Arg> Clone for Box<dyn CloneableFnWithArg<Arg>> {
fn clone(&self) -> Self {
(**self).clone_box()
}
}
impl<Arg> Default for Box<dyn CloneableFnWithArg<Arg>> {
fn default() -> Self {
Box::new(|_| {})
}
}

9
src/utils/demo.rs Normal file
View file

@ -0,0 +1,9 @@
use leptos::document;
use wasm_bindgen::JsCast;
pub fn demo_or_body() -> web_sys::HtmlElement {
document()
.get_element_by_id("demo-anchor")
.map(|e| e.unchecked_into::<web_sys::HtmlElement>())
.unwrap_or(document().body().expect("body to exist"))
}

View file

@ -0,0 +1,80 @@
use crate::utils::CloneableFnWithReturn;
use leptos::leptos_dom::helpers::TimeoutHandle;
use leptos::{set_timeout_with_handle, MaybeSignal, SignalGetUntracked};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::time::Duration;
#[derive(Default)]
pub struct DebounceOptions {
/// The maximum time allowed to be delayed before it's invoked.
/// In milliseconds.
max_wait: MaybeSignal<Option<f64>>,
}
pub fn debounce_filter(
ms: impl Into<MaybeSignal<f64>>,
options: DebounceOptions,
) -> impl FnMut(Box<dyn CloneableFnWithReturn<()>>) -> Rc<RefCell<Option<()>>> {
let timer = Rc::new(Cell::new(None::<TimeoutHandle>));
let max_timer = Rc::new(Cell::new(None::<TimeoutHandle>));
let clear_timeout = move |timer: &Rc<Cell<Option<TimeoutHandle>>>| {
if let Some(handle) = timer.get() {
handle.clear();
timer.set(None);
}
};
let ms = ms.into();
move |invoke: Box<dyn CloneableFnWithReturn<()>>| {
let duration = ms.get_untracked();
let max_duration = options.max_wait.get_untracked();
// TODO : return value like throttle_filter?
clear_timeout(&timer);
if duration <= 0.0 || max_duration.is_some_and(|d| d <= 0.0) {
clear_timeout(&max_timer);
invoke();
return Rc::new(RefCell::new(None));
}
// Create the max_timer. Clears the regular timer on invoke
if let Some(max_duration) = max_duration {
if max_timer.get().is_none() {
let timer = Rc::clone(&timer);
let invok = invoke.clone();
max_timer.set(
set_timeout_with_handle(
move || {
clear_timeout(&timer);
invok();
},
Duration::from_millis(max_duration as u64),
)
.ok(),
);
}
}
let max_timer = Rc::clone(&max_timer);
// Create the regular timer. Clears the max timer on invoke
timer.set(
set_timeout_with_handle(
move || {
clear_timeout(&max_timer);
invoke();
},
Duration::from_millis(duration as u64),
)
.ok(),
);
Rc::new(RefCell::new(None))
}
}

62
src/utils/filters/mod.rs Normal file
View file

@ -0,0 +1,62 @@
mod debounce;
mod throttle;
pub use debounce::*;
pub use throttle::*;
use crate::utils::CloneableFnWithReturn;
use std::cell::RefCell;
use std::rc::Rc;
pub fn create_filter_wrapper<F, Filter>(mut filter: Filter, func: F) -> impl FnMut()
where
F: FnOnce() + Clone + 'static,
Filter: FnMut(Box<dyn CloneableFnWithReturn<()>>) -> Rc<RefCell<Option<()>>>,
{
move || {
filter(Box::new(func.clone()));
}
}
pub fn create_filter_wrapper_with_arg<F, Arg, Filter>(
mut filter: Filter,
func: F,
) -> impl FnMut(Arg)
where
F: FnOnce(Arg) + Clone + 'static,
Arg: Clone + 'static,
Filter: FnMut(Box<dyn CloneableFnWithReturn<()>>) -> Rc<RefCell<Option<()>>>,
{
move |arg: Arg| {
let mut func = func.clone();
filter(Box::new(move || func(arg)));
}
}
pub fn create_filter_wrapper_with_return<F, R, Filter>(
mut filter: Filter,
func: F,
) -> impl FnMut() -> Rc<RefCell<Option<R>>>
where
F: FnOnce() -> R + Clone + 'static,
R: 'static,
Filter: FnMut(Box<dyn CloneableFnWithReturn<R>>) -> Rc<RefCell<Option<R>>>,
{
move || filter(Box::new(func.clone()))
}
pub fn create_filter_wrapper_with_return_and_arg<F, Arg, R, Filter>(
mut filter: Filter,
func: F,
) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>>
where
F: FnOnce(Arg) -> R + Clone + 'static,
R: 'static,
Arg: Clone + 'static,
Filter: FnMut(Box<dyn CloneableFnWithReturn<R>>) -> Rc<RefCell<Option<R>>>,
{
move |arg: Arg| {
let mut func = func.clone();
filter(Box::new(move || func(arg)))
}
}

View file

@ -1,3 +1,4 @@
use crate::utils::CloneableFnWithReturn;
use js_sys::Date; use js_sys::Date;
use leptos::leptos_dom::helpers::TimeoutHandle; use leptos::leptos_dom::helpers::TimeoutHandle;
use leptos::{set_timeout_with_handle, MaybeSignal, SignalGetUntracked}; use leptos::{set_timeout_with_handle, MaybeSignal, SignalGetUntracked};
@ -6,38 +7,6 @@ use std::cmp::max;
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
pub fn create_filter_wrapper<F, R, Filter>(
mut filter: Filter,
func: F,
) -> impl FnMut() -> Rc<RefCell<Option<R>>>
where
F: FnMut() -> R + Clone + 'static,
R: 'static,
Filter: FnMut(Box<dyn FnOnce() -> R>) -> Rc<RefCell<Option<R>>>,
{
move || {
let wrapped_func = Box::new(func.clone());
filter(wrapped_func)
}
}
pub fn create_filter_wrapper_with_arg<F, Arg, R, Filter>(
mut filter: Filter,
func: F,
) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>>
where
F: FnMut(Arg) -> R + Clone + 'static,
R: 'static,
Arg: 'static,
Filter: FnMut(Box<dyn FnOnce() -> R>) -> Rc<RefCell<Option<R>>>,
{
move |arg: Arg| {
let mut func = func.clone();
let wrapped_func = Box::new(move || func(arg));
filter(wrapped_func)
}
}
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct ThrottleOptions { pub struct ThrottleOptions {
pub trailing: bool, pub trailing: bool,
@ -56,7 +25,7 @@ impl Default for ThrottleOptions {
pub fn throttle_filter<R>( pub fn throttle_filter<R>(
ms: impl Into<MaybeSignal<f64>>, ms: impl Into<MaybeSignal<f64>>,
options: ThrottleOptions, options: ThrottleOptions,
) -> impl FnMut(Box<dyn FnOnce() -> R>) -> Rc<RefCell<Option<R>>> ) -> impl FnMut(Box<dyn CloneableFnWithReturn<R>>) -> Rc<RefCell<Option<R>>>
where where
R: 'static, R: 'static,
{ {
@ -75,7 +44,7 @@ where
let ms = ms.into(); let ms = ms.into();
move |mut _invoke: Box<dyn FnOnce() -> R>| { move |mut _invoke: Box<dyn CloneableFnWithReturn<R>>| {
let duration = ms.get_untracked(); let duration = ms.get_untracked();
let elapsed = Date::now() - last_exec.get(); let elapsed = Date::now() - last_exec.get();

View file

@ -1,3 +0,0 @@
pub fn noop() -> Box<dyn FnMut()> {
Box::new(|| {})
}

View file

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