mirror of
https://github.com/adoyle0/leptos-use.git
synced 2025-01-22 16:49:22 -05:00
added better demo integration and styling to book
This commit is contained in:
parent
6a9a0b5927
commit
5057bf0765
31 changed files with 610 additions and 775 deletions
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## 0.1.3
|
||||
|
||||
- Added `use_scroll`.
|
|
@ -14,6 +14,6 @@ repository = "https://github.com/Synphonyte/leptos-use"
|
|||
|
||||
[dependencies]
|
||||
leptos = "0.3"
|
||||
web-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = ["ScrollToOptions", "ScrollBehavior"] }
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
|
@ -7,7 +7,6 @@ title = "Leptos-Use Documentation"
|
|||
|
||||
[output.html]
|
||||
no-section-label = true
|
||||
additional-css = ["custom.css"]
|
||||
additional-js = ["demo-iframe.js"]
|
||||
additional-css = ["src/custom.css", "src/demo.css"]
|
||||
|
||||
[preprocessor.cmdrun]
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -29,6 +29,32 @@ def build_and_copy_demo(category, md_name):
|
|||
shutil.copytree(example_output_path, target_path,
|
||||
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__':
|
||||
main()
|
||||
|
|
8
docs/book/src/custom.css
Normal file
8
docs/book/src/custom.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
.light {
|
||||
--fg: #333;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
border-radius: 5px;
|
||||
padding: 1.5rem;
|
||||
}
|
66
docs/book/src/demo.css
Normal file
66
docs/book/src/demo.css
Normal 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;
|
||||
}
|
|
@ -34,9 +34,8 @@ def process_line(line, name):
|
|||
if stripped.startswith("[Link to Demo](https://"):
|
||||
example_link = stripped.replace("[Link to Demo](", "").replace(")", "")
|
||||
result = f'''<div class="demo-container">
|
||||
<a class="demo-source" href="{example_link}/src/main.rs" target="_blank">source</a>
|
||||
<iframe class="demo" src="{name}/demo/index.html" width="100%" frameborder="0">
|
||||
</iframe>
|
||||
<a class="demo-source" href="{example_link}/src/main.rs" target="_blank">source <i class="fa fa-github"></i></a>
|
||||
<div id="demo-anchor"></div>
|
||||
</div>'''
|
||||
|
||||
else:
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
# Functions
|
||||
|
||||
<!-- cmdrun python3 generate_function_overview.py browser -->
|
||||
|
||||
<!-- cmdrun python3 generate_function_overview.py sensors -->
|
||||
|
||||
<!-- cmdrun python3 generate_function_overview.py utilities -->
|
||||
|
|
3
docs/book/src/sensors/use_scroll.md
Normal file
3
docs/book/src/sensors/use_scroll.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# use_event_listener
|
||||
|
||||
<!-- cmdrun python3 ../extract_doc_comment.py use_scroll -->
|
|
@ -1,22 +1,16 @@
|
|||
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:
|
||||
|
||||
```bash
|
||||
cargo install trunk
|
||||
npm install -D tailwindcss
|
||||
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:
|
||||
To run the demo:
|
||||
|
||||
```bash
|
||||
trunk serve --open
|
||||
```
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
[build]
|
||||
public_url = "/leptos-use/utilities/use_throttle_fn/demo/"
|
||||
public_url = "./demo/"
|
|
@ -1,7 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="css" href="style/output.css">
|
||||
</head>
|
||||
<body></body>
|
||||
<head></head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -1,5 +1,6 @@
|
|||
use leptos::*;
|
||||
use leptos_use::use_throttle_fn;
|
||||
use leptos_use::utils::demo_or_body;
|
||||
|
||||
#[component]
|
||||
fn Demo(cx: Scope) -> impl IntoView {
|
||||
|
@ -15,12 +16,11 @@ fn Demo(cx: Scope) -> impl IntoView {
|
|||
set_click_count(click_count() + 1);
|
||||
throttled_fn();
|
||||
}
|
||||
class="rounded bg-blue-500 hover:bg-blue-400 py-2 px-4 text-white"
|
||||
>
|
||||
"Smash me!"
|
||||
</button>
|
||||
<p class="my-2"><small class="block">"Delay is set to 1000ms for this demo."</small></p>
|
||||
<p class="my-3">"Button clicked: " { click_count }</p>
|
||||
<div class="note">"Delay is set to 1000ms for this demo."</div>
|
||||
<p>"Button clicked: " { click_count }</p>
|
||||
<p>"Event handler called: " { throttled_count }</p>
|
||||
}
|
||||
}
|
||||
|
@ -29,11 +29,7 @@ fn main() {
|
|||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
mount_to_body(|cx| {
|
||||
view! {cx,
|
||||
<div class="p-6 bg-gray-700 text-gray-300">
|
||||
<Demo />
|
||||
</div>
|
||||
}
|
||||
mount_to(demo_or_body(), |cx| {
|
||||
view! { cx, <Demo /> }
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
|
@ -1,51 +1,55 @@
|
|||
use leptos::html::ElementDescriptor;
|
||||
use leptos::*;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// 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,
|
||||
/// * a `Signal<T>` where `T` is the web_sys element,
|
||||
/// * a `Signal<Option<T>>` where `T` is the web_sys element,
|
||||
/// * a `NodeRef`
|
||||
/// into a function. Used for example in [`use_event_listener`].
|
||||
pub enum EventTargetMaybeSignal<T>
|
||||
pub enum ElementMaybeSignal<T, E>
|
||||
where
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
Static(Option<T>),
|
||||
Dynamic(Signal<Option<T>>),
|
||||
_Phantom(PhantomData<E>),
|
||||
}
|
||||
|
||||
impl<T> Default for EventTargetMaybeSignal<T>
|
||||
impl<T, E> Default for ElementMaybeSignal<T, E>
|
||||
where
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::Static(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for EventTargetMaybeSignal<T>
|
||||
impl<T, E> Clone for ElementMaybeSignal<T, E>
|
||||
where
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Static(t) => Self::Static(t.clone()),
|
||||
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
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
fn get(&self) -> Option<T> {
|
||||
match self {
|
||||
Self::Static(t) => t.clone(),
|
||||
Self::Dynamic(s) => s.get(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,18 +57,20 @@ where
|
|||
match self {
|
||||
Self::Static(t) => Some(t.clone()),
|
||||
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
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
fn with<O>(&self, f: impl FnOnce(&Option<T>) -> O) -> O {
|
||||
match self {
|
||||
Self::Static(t) => f(t),
|
||||
Self::Dynamic(s) => s.with(f),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,18 +78,20 @@ where
|
|||
match self {
|
||||
Self::Static(t) => Some(f(t)),
|
||||
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
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&Option<T>) -> O) -> O {
|
||||
match self {
|
||||
Self::Static(t) => f(t),
|
||||
Self::Dynamic(s) => s.with_untracked(f),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,18 +99,20 @@ where
|
|||
match self {
|
||||
Self::Static(t) => Some(f(t)),
|
||||
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
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
fn get_untracked(&self) -> Option<T> {
|
||||
match self {
|
||||
Self::Static(t) => t.clone(),
|
||||
Self::Dynamic(s) => s.get_untracked(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,36 +120,37 @@ where
|
|||
match self {
|
||||
Self::Static(t) => Some(t.clone()),
|
||||
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
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
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
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
fn from(target: (Scope, Option<T>)) -> Self {
|
||||
EventTargetMaybeSignal::Static(target.1)
|
||||
ElementMaybeSignal::Static(target.1)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from_signal_option {
|
||||
($ty:ty) => {
|
||||
impl<T> From<(Scope, $ty)> for EventTargetMaybeSignal<T>
|
||||
impl<T, E> From<(Scope, $ty)> for ElementMaybeSignal<T, E>
|
||||
where
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
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 {
|
||||
($ty:ty) => {
|
||||
impl<T> From<(Scope, $ty)> for EventTargetMaybeSignal<T>
|
||||
impl<T, E> From<(Scope, $ty)> for ElementMaybeSignal<T, E>
|
||||
where
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
T: Into<E> + Clone + 'static,
|
||||
{
|
||||
fn from(target: (Scope, $ty)) -> Self {
|
||||
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!(Memo<T>);
|
||||
|
||||
impl<R> From<(Scope, NodeRef<R>)> for EventTargetMaybeSignal<web_sys::EventTarget>
|
||||
where
|
||||
R: ElementDescriptor + Clone + 'static,
|
||||
{
|
||||
fn from(target: (Scope, NodeRef<R>)) -> Self {
|
||||
let (cx, node_ref) = target;
|
||||
macro_rules! impl_from_node_ref {
|
||||
($ty:ty) => {
|
||||
impl<R> From<(Scope, NodeRef<R>)> for ElementMaybeSignal<$ty, $ty>
|
||||
where
|
||||
R: ElementDescriptor + Clone + 'static,
|
||||
{
|
||||
fn from(target: (Scope, NodeRef<R>)) -> Self {
|
||||
let (cx, node_ref) = target;
|
||||
|
||||
EventTargetMaybeSignal::Dynamic(Signal::derive(cx, move || {
|
||||
node_ref.get().map(move |el| {
|
||||
let el = el.into_any();
|
||||
let el: web_sys::EventTarget = el.deref().clone().into();
|
||||
el
|
||||
})
|
||||
}))
|
||||
}
|
||||
ElementMaybeSignal::Dynamic(Signal::derive(cx, move || {
|
||||
node_ref.get().map(move |el| {
|
||||
let el = el.into_any();
|
||||
let el: $ty = el.deref().clone().into();
|
||||
el
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_from_node_ref!(web_sys::EventTarget);
|
||||
impl_from_node_ref!(web_sys::Element);
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
pub mod core;
|
||||
pub mod use_debounce_fn;
|
||||
pub mod use_event_listener;
|
||||
pub mod use_scroll;
|
||||
pub mod use_throttle_fn;
|
||||
pub mod utils;
|
||||
|
||||
pub use use_debounce_fn::*;
|
||||
pub use use_event_listener::use_event_listener;
|
||||
pub use use_scroll::*;
|
||||
pub use use_throttle_fn::*;
|
||||
|
|
42
src/use_debounce_fn.rs
Normal file
42
src/use_debounce_fn.rs
Normal 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)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use crate::core::EventTargetMaybeSignal;
|
||||
use crate::core::ElementMaybeSignal;
|
||||
use leptos::ev::EventDescriptor;
|
||||
use leptos::*;
|
||||
use std::cell::RefCell;
|
||||
|
@ -90,7 +90,7 @@ pub fn use_event_listener<Ev, El, T, F>(
|
|||
) -> Box<dyn Fn()>
|
||||
where
|
||||
Ev: EventDescriptor + 'static,
|
||||
(Scope, El): Into<EventTargetMaybeSignal<T>>,
|
||||
(Scope, El): Into<ElementMaybeSignal<T, web_sys::EventTarget>>,
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
F: FnMut(<Ev as EventDescriptor>::EventType) + 'static,
|
||||
{
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
/// 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)]
|
||||
pub fn use_scroll<El, T, Fx, Fy>(
|
||||
pub fn use_scroll_with_options<El, T>(
|
||||
cx: Scope,
|
||||
element: El,
|
||||
options: UseScrollOptions,
|
||||
) -> UseScrollReturn
|
||||
where
|
||||
(Scope, El): Into<EventTargetMaybeSignal<T>>,
|
||||
T: Into<web_sys::EventTarget> + Clone + 'static,
|
||||
Fx: Fn(f64),
|
||||
Fy: Fn(f64),
|
||||
(Scope, El): Into<ElementMaybeSignal<T, web_sys::Element>>,
|
||||
T: Into<web_sys::Element> + Clone + 'static,
|
||||
{
|
||||
// TODO : implement
|
||||
|
||||
let (x, set_x) = create_signal(cx, 0.0);
|
||||
let (y, set_y) = create_signal(cx, 0.0);
|
||||
let (internal_x, set_internal_x) = create_signal(cx, 0.0);
|
||||
let (internal_y, set_internal_y) = create_signal(cx, 0.0);
|
||||
|
||||
let (is_scrolling, _) = create_signal(cx, false);
|
||||
let (arrived_state, _) = create_signal(
|
||||
cx,
|
||||
Directions {
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
bottom: false,
|
||||
},
|
||||
);
|
||||
let (directions, _) = create_signal(
|
||||
let signal = (cx, element).into();
|
||||
let behavior = options.behavior;
|
||||
|
||||
let scroll_to = move |x: Option<f64>, y: Option<f64>| {
|
||||
let element = signal.get_untracked();
|
||||
|
||||
if let Some(element) = element {
|
||||
let element = element.into();
|
||||
|
||||
let mut scroll_options = web_sys::ScrollToOptions::new();
|
||||
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,
|
||||
Directions {
|
||||
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 {
|
||||
x: x.into(),
|
||||
set_x: Box::new(move |x| set_x.set(x)),
|
||||
y: y.into(),
|
||||
set_y: Box::new(move |y| set_y.set(y)),
|
||||
x: internal_x.into(),
|
||||
set_x,
|
||||
y: internal_y.into(),
|
||||
set_y,
|
||||
is_scrolling: is_scrolling.into(),
|
||||
arrived_state: arrived_state.into(),
|
||||
directions: directions.into(),
|
||||
|
@ -50,39 +103,61 @@ where
|
|||
}
|
||||
|
||||
/// Options for [`use_scroll`].
|
||||
#[derive(Default)]
|
||||
pub struct UseScrollOptions {
|
||||
/// 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.
|
||||
/// Defaults to 200.
|
||||
pub idle: u32,
|
||||
pub idle: f64,
|
||||
|
||||
/// Threshold in pixels when we consider a side to have arrived (`UseScrollReturn::arrived_state`).
|
||||
pub offset: ScrollOffset,
|
||||
|
||||
/// 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).
|
||||
pub on_stop: Option<Box<dyn Fn()>>,
|
||||
pub on_stop: Box<dyn CloneableFnWithArg<web_sys::Event>>,
|
||||
|
||||
/// 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.
|
||||
/// Can be `Auto` (= not smooth) or `Smooth`. Defaults to `Auto`.
|
||||
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 {
|
||||
#[default]
|
||||
Auto,
|
||||
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 x: Signal<f64>,
|
||||
pub set_x: Box<dyn Fn(f64)>,
|
||||
|
@ -101,7 +176,7 @@ pub struct Directions {
|
|||
pub bottom: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
pub struct ScrollOffset {
|
||||
pub left: f64,
|
||||
pub top: f64,
|
||||
|
|
|
@ -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 std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
@ -41,10 +43,9 @@ pub use crate::utils::ThrottleOptions;
|
|||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::{ThrottleOptions, use_throttle_fn_with_options};
|
||||
///
|
||||
/// # #[component]
|
||||
/// # 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
|
||||
/// },
|
||||
|
@ -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`].
|
||||
///
|
||||
/// ## Recommended Reading
|
||||
|
@ -69,7 +70,7 @@ pub fn use_throttle_fn<F, R>(
|
|||
ms: impl Into<MaybeSignal<f64>>,
|
||||
) -> impl FnMut() -> Rc<RefCell<Option<R>>>
|
||||
where
|
||||
F: FnMut() -> R + Clone + 'static,
|
||||
F: FnOnce() -> R + Clone + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
use_throttle_fn_with_options(func, ms, Default::default())
|
||||
|
@ -82,10 +83,10 @@ pub fn use_throttle_fn_with_options<F, R>(
|
|||
options: ThrottleOptions,
|
||||
) -> impl FnMut() -> Rc<RefCell<Option<R>>>
|
||||
where
|
||||
F: FnMut() -> R + Clone + 'static,
|
||||
F: FnOnce() -> R + Clone + '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.
|
||||
|
@ -94,8 +95,8 @@ pub fn use_throttle_fn_with_arg<F, Arg, R>(
|
|||
ms: impl Into<MaybeSignal<f64>>,
|
||||
) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>>
|
||||
where
|
||||
F: FnMut(Arg) -> R + Clone + 'static,
|
||||
Arg: 'static,
|
||||
F: FnOnce(Arg) -> R + Clone + 'static,
|
||||
Arg: Clone + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
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,
|
||||
) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>>
|
||||
where
|
||||
F: FnMut(Arg) -> R + Clone + 'static,
|
||||
Arg: 'static,
|
||||
F: FnOnce(Arg) -> R + Clone + 'static,
|
||||
Arg: Clone + '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
101
src/utils/clonable_fn.rs
Normal 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
9
src/utils/demo.rs
Normal 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"))
|
||||
}
|
80
src/utils/filters/debounce.rs
Normal file
80
src/utils/filters/debounce.rs
Normal 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
62
src/utils/filters/mod.rs
Normal 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)))
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
use crate::utils::CloneableFnWithReturn;
|
||||
use js_sys::Date;
|
||||
use leptos::leptos_dom::helpers::TimeoutHandle;
|
||||
use leptos::{set_timeout_with_handle, MaybeSignal, SignalGetUntracked};
|
||||
|
@ -6,38 +7,6 @@ use std::cmp::max;
|
|||
use std::rc::Rc;
|
||||
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)]
|
||||
pub struct ThrottleOptions {
|
||||
pub trailing: bool,
|
||||
|
@ -56,7 +25,7 @@ impl Default for ThrottleOptions {
|
|||
pub fn throttle_filter<R>(
|
||||
ms: impl Into<MaybeSignal<f64>>,
|
||||
options: ThrottleOptions,
|
||||
) -> impl FnMut(Box<dyn FnOnce() -> R>) -> Rc<RefCell<Option<R>>>
|
||||
) -> impl FnMut(Box<dyn CloneableFnWithReturn<R>>) -> Rc<RefCell<Option<R>>>
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
|
@ -75,7 +44,7 @@ where
|
|||
|
||||
let ms = ms.into();
|
||||
|
||||
move |mut _invoke: Box<dyn FnOnce() -> R>| {
|
||||
move |mut _invoke: Box<dyn CloneableFnWithReturn<R>>| {
|
||||
let duration = ms.get_untracked();
|
||||
let elapsed = Date::now() - last_exec.get();
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
pub fn noop() -> Box<dyn FnMut()> {
|
||||
Box::new(|| {})
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
mod clonable_fn;
|
||||
mod demo;
|
||||
mod filters;
|
||||
mod is;
|
||||
|
||||
pub use clonable_fn::*;
|
||||
pub use demo::*;
|
||||
pub use filters::*;
|
||||
pub use is::*;
|
||||
|
|
Loading…
Add table
Reference in a new issue