Merge branch 'DioxusLabs:master' into select_example
This commit is contained in:
39 files changed
+1167
-120
No files matched your search
@@ -23,6 +23,7 @@ members = [
|
|||||||
"packages/signals",
|
"packages/signals",
|
||||||
"packages/hot-reload",
|
"packages/hot-reload",
|
||||||
"docs/guide",
|
"docs/guide",
|
||||||
|
"examples/PWA-example",
|
||||||
]
|
]
|
||||||
|
|
||||||
# This is a "virtual package"
|
# This is a "virtual package"
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ fn App(cx: Scope) -> Element {
|
|||||||
// ANCHOR: prevent_default
|
// ANCHOR: prevent_default
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
input {
|
input {
|
||||||
prevent_default: "oninput",
|
prevent_default: "oninput onclick",
|
||||||
prevent_default: "onclick",
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// ANCHOR_END: prevent_default
|
// ANCHOR_END: prevent_default
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Some events will trigger first on the element the event originated at upward. Fo
|
|||||||
|
|
||||||
> For more information about event propigation see [the mdn docs on event bubling](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling)
|
> For more information about event propigation see [the mdn docs on event bubling](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling)
|
||||||
|
|
||||||
If you want to prevent this behavior, you can call `stop_propogation()` on the event:
|
If you want to prevent this behavior, you can call `stop_propagation()` on the event:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/event_nested.rs:rsx}}
|
{{#include ../../../examples/event_nested.rs:rsx}}
|
||||||
@@ -41,7 +41,7 @@ If you want to prevent this behavior, you can call `stop_propogation()` on the e
|
|||||||
|
|
||||||
Some events have a default behavior. For keyboard events, this might be entering the typed character. For mouse events, this might be selecting some text.
|
Some events have a default behavior. For keyboard events, this might be entering the typed character. For mouse events, this might be selecting some text.
|
||||||
|
|
||||||
In some instances, might want to avoid this default behavior. For this, you can add the `prevent_default` attribute with the name of the handler whose default behavior you want to stop. This attribute is special: you can attach it multiple times for multiple attributes:
|
In some instances, might want to avoid this default behavior. For this, you can add the `prevent_default` attribute with the name of the handler whose default behavior you want to stop. This attribute can be used for multiple handlers using their name separated by spaces:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
{{#include ../../../examples/event_prevent_default.rs:prevent_default}}
|
{{#include ../../../examples/event_prevent_default.rs:prevent_default}}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "dioxus-pwa-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Antonio Curavalea <one.kyonblack@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dioxus = { path = "../../packages/dioxus", version = "^0.3.0"}
|
||||||
|
dioxus-web = { path = "../../packages/web", version = "^0.3.0"}
|
||||||
|
|
||||||
|
log = "0.4.6"
|
||||||
|
|
||||||
|
# WebAssembly Debug
|
||||||
|
wasm-logger = "0.2.0"
|
||||||
|
console_error_panic_hook = "0.1.7"
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
[application]
|
||||||
|
|
||||||
|
# App (Project) Name
|
||||||
|
name = "dioxus-pwa-example"
|
||||||
|
|
||||||
|
# Dioxus App Default Platform
|
||||||
|
# desktop, web, mobile, ssr
|
||||||
|
default_platform = "web"
|
||||||
|
|
||||||
|
# `build` & `serve` dist path
|
||||||
|
out_dir = "dist"
|
||||||
|
|
||||||
|
# resource (public) file folder
|
||||||
|
asset_dir = "public"
|
||||||
|
|
||||||
|
[web.app]
|
||||||
|
|
||||||
|
# HTML title tag content
|
||||||
|
title = "dioxus | ⛺"
|
||||||
|
|
||||||
|
[web.watcher]
|
||||||
|
|
||||||
|
# when watcher trigger, regenerate the `index.html`
|
||||||
|
reload_html = true
|
||||||
|
|
||||||
|
# which files or dirs will be watcher monitoring
|
||||||
|
watch_path = ["src", "public"]
|
||||||
|
|
||||||
|
# include `assets` in web platform
|
||||||
|
[web.resource]
|
||||||
|
|
||||||
|
# CSS style file
|
||||||
|
style = []
|
||||||
|
|
||||||
|
# Javascript code file
|
||||||
|
script = []
|
||||||
|
|
||||||
|
[web.resource.dev]
|
||||||
|
|
||||||
|
# Javascript code file
|
||||||
|
# serve: [dev-server] only
|
||||||
|
script = []
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Dioxus
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Dioxus PWA example
|
||||||
|
|
||||||
|
This is a basic example of a progressive web app (PWA) using Dioxus and Dioxus CLI.
|
||||||
|
Currently PWA functionality requires the use of a service worker and manifest file, so this isn't 100% Rust yet.
|
||||||
|
|
||||||
|
It is also very much usable as a template for your projects, if you're aiming to create a PWA.
|
||||||
|
|
||||||
|
## Try the example
|
||||||
|
|
||||||
|
Make sure you have Dioxus CLI installed (if you're unsure, run `cargo install dioxus-cli`).
|
||||||
|
|
||||||
|
You can run `dioxus serve` in this directory to start the web server locally, or run
|
||||||
|
`dioxus build --release` to build the project so you can deploy it on a separate web-server.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
```
|
||||||
|
├── Cargo.toml
|
||||||
|
├── Dioxus.toml
|
||||||
|
├── index.html // Custom HTML is needed for this, to load the SW and manifest.
|
||||||
|
├── LICENSE
|
||||||
|
├── public
|
||||||
|
│ ├── favicon.ico
|
||||||
|
│ ├── logo_192.png
|
||||||
|
│ ├── logo_512.png
|
||||||
|
│ ├── manifest.json // The manifest file - edit this as you need to.
|
||||||
|
│ └── sw.js // The service worker - you must edit this for actual projects.
|
||||||
|
├── README.md
|
||||||
|
└── src
|
||||||
|
└── main.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
If you're just getting started with PWAs, here are some useful resources:
|
||||||
|
|
||||||
|
* [PWABuilder docs](https://docs.pwabuilder.com/#/)
|
||||||
|
* [MDN article on PWAs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
|
||||||
|
|
||||||
|
For service worker scripting (in JavaScript):
|
||||||
|
|
||||||
|
* [Service worker guide from PWABuilder](https://docs.pwabuilder.com/#/home/sw-intro)
|
||||||
|
* [Service worker examples, also from PWABuilder](https://github.com/pwa-builder/pwabuilder-serviceworkers)
|
||||||
|
|
||||||
|
If you want to stay as close to 100% Rust as possible, you can try using [wasi-worker](https://github.com/dunnock/wasi-worker) to replace the JS service worker file. The JSON manifest will still be required though.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{app_title}</title>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register(
|
||||||
|
'/sw.js'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
{style_include}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main"></div>
|
||||||
|
<script type="module">
|
||||||
|
import init from "/{base_path}/assets/dioxus/{app_name}.js";
|
||||||
|
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {
|
||||||
|
if (wasm.__wbindgen_start == undefined) {
|
||||||
|
wasm.main();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{script_include}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "Dioxus",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "logo_192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo_512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo_512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "any",
|
||||||
|
"purpose": "any"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "/",
|
||||||
|
"id": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"display_override": ["window-control-overlay", "standalone"],
|
||||||
|
"scope": "/",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"short_name": "Dioxus",
|
||||||
|
"description": "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust.",
|
||||||
|
"dir": "ltr",
|
||||||
|
"lang": "en",
|
||||||
|
"orientation": "portrait"
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
//console.log('WORKER: executing.');
|
||||||
|
|
||||||
|
/* A version number is useful when updating the worker logic,
|
||||||
|
allowing you to remove outdated cache entries during the update.
|
||||||
|
*/
|
||||||
|
var version = 'v1.0.0::';
|
||||||
|
|
||||||
|
/* These resources will be downloaded and cached by the service worker
|
||||||
|
during the installation process. If any resource fails to be downloaded,
|
||||||
|
then the service worker won't be installed either.
|
||||||
|
*/
|
||||||
|
var offlineFundamentals = [
|
||||||
|
// add here the files you want to cache
|
||||||
|
'favicon.ico'
|
||||||
|
];
|
||||||
|
|
||||||
|
/* The install event fires when the service worker is first installed.
|
||||||
|
You can use this event to prepare the service worker to be able to serve
|
||||||
|
files while visitors are offline.
|
||||||
|
*/
|
||||||
|
self.addEventListener("install", function (event) {
|
||||||
|
//console.log('WORKER: install event in progress.');
|
||||||
|
/* Using event.waitUntil(p) blocks the installation process on the provided
|
||||||
|
promise. If the promise is rejected, the service worker won't be installed.
|
||||||
|
*/
|
||||||
|
event.waitUntil(
|
||||||
|
/* The caches built-in is a promise-based API that helps you cache responses,
|
||||||
|
as well as finding and deleting them.
|
||||||
|
*/
|
||||||
|
caches
|
||||||
|
/* You can open a cache by name, and this method returns a promise. We use
|
||||||
|
a versioned cache name here so that we can remove old cache entries in
|
||||||
|
one fell swoop later, when phasing out an older service worker.
|
||||||
|
*/
|
||||||
|
.open(version + 'fundamentals')
|
||||||
|
.then(function (cache) {
|
||||||
|
/* After the cache is opened, we can fill it with the offline fundamentals.
|
||||||
|
The method below will add all resources in `offlineFundamentals` to the
|
||||||
|
cache, after making requests for them.
|
||||||
|
*/
|
||||||
|
return cache.addAll(offlineFundamentals);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
//console.log('WORKER: install completed');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* The fetch event fires whenever a page controlled by this service worker requests
|
||||||
|
a resource. This isn't limited to `fetch` or even XMLHttpRequest. Instead, it
|
||||||
|
comprehends even the request for the HTML page on first load, as well as JS and
|
||||||
|
CSS resources, fonts, any images, etc.
|
||||||
|
*/
|
||||||
|
self.addEventListener("fetch", function (event) {
|
||||||
|
//console.log('WORKER: fetch event in progress.');
|
||||||
|
|
||||||
|
/* We should only cache GET requests, and deal with the rest of method in the
|
||||||
|
client-side, by handling failed POST,PUT,PATCH,etc. requests.
|
||||||
|
*/
|
||||||
|
if (event.request.method !== 'GET') {
|
||||||
|
/* If we don't block the event as shown below, then the request will go to
|
||||||
|
the network as usual.
|
||||||
|
*/
|
||||||
|
//console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Similar to event.waitUntil in that it blocks the fetch event on a promise.
|
||||||
|
Fulfillment result will be used as the response, and rejection will end in a
|
||||||
|
HTTP response indicating failure.
|
||||||
|
*/
|
||||||
|
event.respondWith(
|
||||||
|
caches
|
||||||
|
/* This method returns a promise that resolves to a cache entry matching
|
||||||
|
the request. Once the promise is settled, we can then provide a response
|
||||||
|
to the fetch request.
|
||||||
|
*/
|
||||||
|
.match(event.request)
|
||||||
|
.then(function (cached) {
|
||||||
|
/* Even if the response is in our cache, we go to the network as well.
|
||||||
|
This pattern is known for producing "eventually fresh" responses,
|
||||||
|
where we return cached responses immediately, and meanwhile pull
|
||||||
|
a network response and store that in the cache.
|
||||||
|
|
||||||
|
Read more:
|
||||||
|
https://ponyfoo.com/articles/progressive-networking-serviceworker
|
||||||
|
*/
|
||||||
|
var networked = fetch(event.request)
|
||||||
|
// We handle the network request with success and failure scenarios.
|
||||||
|
.then(fetchedFromNetwork, unableToResolve)
|
||||||
|
// We should catch errors on the fetchedFromNetwork handler as well.
|
||||||
|
.catch(unableToResolve);
|
||||||
|
|
||||||
|
/* We return the cached response immediately if there is one, and fall
|
||||||
|
back to waiting on the network as usual.
|
||||||
|
*/
|
||||||
|
//console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
|
||||||
|
return cached || networked;
|
||||||
|
|
||||||
|
function fetchedFromNetwork(response) {
|
||||||
|
/* We copy the response before replying to the network request.
|
||||||
|
This is the response that will be stored on the ServiceWorker cache.
|
||||||
|
*/
|
||||||
|
var cacheCopy = response.clone();
|
||||||
|
|
||||||
|
//console.log('WORKER: fetch response from network.', event.request.url);
|
||||||
|
|
||||||
|
caches
|
||||||
|
// We open a cache to store the response for this request.
|
||||||
|
.open(version + 'pages')
|
||||||
|
.then(function add(cache) {
|
||||||
|
/* We store the response for this request. It'll later become
|
||||||
|
available to caches.match(event.request) calls, when looking
|
||||||
|
for cached responses.
|
||||||
|
*/
|
||||||
|
cache.put(event.request, cacheCopy);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
//console.log('WORKER: fetch response stored in cache.', event.request.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the response so that the promise is settled in fulfillment.
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When this method is called, it means we were unable to produce a response
|
||||||
|
from either the cache or the network. This is our opportunity to produce
|
||||||
|
a meaningful response even when all else fails. It's the last chance, so
|
||||||
|
you probably want to display a "Service Unavailable" view or a generic
|
||||||
|
error response.
|
||||||
|
*/
|
||||||
|
function unableToResolve() {
|
||||||
|
/* There's a couple of things we can do here.
|
||||||
|
- Test the Accept header and then return one of the `offlineFundamentals`
|
||||||
|
e.g: `return caches.match('/some/cached/image.png')`
|
||||||
|
- You should also consider the origin. It's easier to decide what
|
||||||
|
"unavailable" means for requests against your origins than for requests
|
||||||
|
against a third party, such as an ad provider.
|
||||||
|
- Generate a Response programmaticaly, as shown below, and return that.
|
||||||
|
*/
|
||||||
|
|
||||||
|
//console.log('WORKER: fetch request failed in both cache and network.');
|
||||||
|
|
||||||
|
/* Here we're creating a response programmatically. The first parameter is the
|
||||||
|
response body, and the second one defines the options for the response.
|
||||||
|
*/
|
||||||
|
return new Response('<h1>Service Unavailable</h1>', {
|
||||||
|
status: 503,
|
||||||
|
statusText: 'Service Unavailable',
|
||||||
|
headers: new Headers({
|
||||||
|
'Content-Type': 'text/html'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* The activate event fires after a service worker has been successfully installed.
|
||||||
|
It is most useful when phasing out an older version of a service worker, as at
|
||||||
|
this point you know that the new worker was installed correctly. In this example,
|
||||||
|
we delete old caches that don't match the version in the worker we just finished
|
||||||
|
installing.
|
||||||
|
*/
|
||||||
|
self.addEventListener("activate", function (event) {
|
||||||
|
/* Just like with the install event, event.waitUntil blocks activate on a promise.
|
||||||
|
Activation will fail unless the promise is fulfilled.
|
||||||
|
*/
|
||||||
|
//console.log('WORKER: activate event in progress.');
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
caches
|
||||||
|
/* This method returns a promise which will resolve to an array of available
|
||||||
|
cache keys.
|
||||||
|
*/
|
||||||
|
.keys()
|
||||||
|
.then(function (keys) {
|
||||||
|
// We return a promise that settles when all outdated caches are deleted.
|
||||||
|
return Promise.all(
|
||||||
|
keys
|
||||||
|
.filter(function (key) {
|
||||||
|
// Filter by keys that don't start with the latest version prefix.
|
||||||
|
return !key.startsWith(version);
|
||||||
|
})
|
||||||
|
.map(function (key) {
|
||||||
|
/* Return a promise that's fulfilled
|
||||||
|
when each outdated cache is deleted.
|
||||||
|
*/
|
||||||
|
return caches.delete(key);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
//console.log('WORKER: activate completed.');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// init debug tool for WebAssembly
|
||||||
|
wasm_logger::init(wasm_logger::Config::default());
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
dioxus_web::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! (
|
||||||
|
div {
|
||||||
|
style: "text-align: center;",
|
||||||
|
h1 { "🌗 Dioxus 🚀" }
|
||||||
|
h3 { "Frontend that scales." }
|
||||||
|
p { "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust." }
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(App);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn App(cx: Scope) -> Element {
|
||||||
|
let files_uploaded: &UseRef<Vec<String>> = use_ref(cx, Vec::new);
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
input {
|
||||||
|
r#type: "file",
|
||||||
|
accept: ".txt, .rs",
|
||||||
|
multiple: true,
|
||||||
|
onchange: |evt| {
|
||||||
|
to_owned![files_uploaded];
|
||||||
|
async move {
|
||||||
|
if let Some(file_engine) = &evt.files {
|
||||||
|
let files = file_engine.files();
|
||||||
|
for file_name in &files {
|
||||||
|
if let Some(file) = file_engine.read_file_to_string(file_name).await{
|
||||||
|
files_uploaded.write().push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
for file in files_uploaded.read().iter() {
|
||||||
|
li { "{file}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -5,9 +7,20 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
cx.render(rsx! { generic_child::<i32>{} })
|
cx.render(rsx! { generic_child {
|
||||||
|
data: 0i32
|
||||||
|
} })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generic_child<T>(cx: Scope) -> Element {
|
#[derive(PartialEq, Props)]
|
||||||
cx.render(rsx! { div {} })
|
struct GenericChildProps<T: Display + PartialEq> {
|
||||||
|
data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generic_child<T: Display + PartialEq>(cx: Scope<GenericChildProps<T>>) -> Element {
|
||||||
|
let data = &cx.props.data;
|
||||||
|
|
||||||
|
cx.render(rsx! { div {
|
||||||
|
"{data}"
|
||||||
|
} })
|
||||||
}
|
}
|
||||||
@@ -276,13 +276,12 @@ impl Writer<'_> {
|
|||||||
let start = location.start();
|
let start = location.start();
|
||||||
let line_start = start.line - 1;
|
let line_start = start.line - 1;
|
||||||
|
|
||||||
let this_line = self.src[line_start];
|
let beginning = self
|
||||||
|
.src
|
||||||
let beginning = if this_line.len() > start.column {
|
.get(line_start)
|
||||||
this_line[..start.column].trim()
|
.filter(|this_line| this_line.len() > start.column)
|
||||||
} else {
|
.map(|this_line| this_line[..start.column].trim())
|
||||||
""
|
.unwrap_or_default();
|
||||||
};
|
|
||||||
|
|
||||||
beginning.is_empty()
|
beginning.is_empty()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -384,51 +384,73 @@ impl VirtualDom {
|
|||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Loop through each dynamic attribute in this template before moving up to the template's parent.
|
// If the event bubbles, we traverse through the tree until we find the target element.
|
||||||
while let Some(el_ref) = parent_path {
|
if bubbles {
|
||||||
// safety: we maintain references of all vnodes in the element slab
|
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
|
||||||
let template = unsafe { el_ref.template.unwrap().as_ref() };
|
while let Some(el_ref) = parent_path {
|
||||||
let node_template = template.template.get();
|
// safety: we maintain references of all vnodes in the element slab
|
||||||
let target_path = el_ref.path;
|
let template = unsafe { el_ref.template.unwrap().as_ref() };
|
||||||
|
let node_template = template.template.get();
|
||||||
|
let target_path = el_ref.path;
|
||||||
|
|
||||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||||
let this_path = node_template.attr_paths[idx];
|
let this_path = node_template.attr_paths[idx];
|
||||||
|
|
||||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||||
if attr.name.trim_start_matches("on") == name
|
if attr.name.trim_start_matches("on") == name
|
||||||
&& target_path.is_decendant(&this_path)
|
&& target_path.is_decendant(&this_path)
|
||||||
{
|
{
|
||||||
listeners.push(&attr.value);
|
listeners.push(&attr.value);
|
||||||
|
|
||||||
// Break if the event doesn't bubble anyways
|
// Break if this is the exact target element.
|
||||||
if !bubbles {
|
// This means we won't call two listeners with the same name on the same element. This should be
|
||||||
break;
|
// documented, or be rejected from the rsx! macro outright
|
||||||
|
if target_path == this_path {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Break if this is the exact target element.
|
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
|
||||||
// This means we won't call two listeners with the same name on the same element. This should be
|
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
||||||
// documented, or be rejected from the rsx! macro outright
|
for listener in listeners.drain(..).rev() {
|
||||||
if target_path == this_path {
|
if let AttributeValue::Listener(listener) = listener {
|
||||||
break;
|
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||||
|
cb(uievent.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !uievent.propagates.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise, we just call the listener on the target element
|
||||||
|
if let Some(el_ref) = parent_path {
|
||||||
|
// safety: we maintain references of all vnodes in the element slab
|
||||||
|
let template = unsafe { el_ref.template.unwrap().as_ref() };
|
||||||
|
let node_template = template.template.get();
|
||||||
|
let target_path = el_ref.path;
|
||||||
|
|
||||||
|
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||||
|
let this_path = node_template.attr_paths[idx];
|
||||||
|
|
||||||
|
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||||
|
// Only call the listener if this is the exact target element.
|
||||||
|
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
||||||
|
if let AttributeValue::Listener(listener) = &attr.value {
|
||||||
|
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||||
|
cb(uievent.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
|
|
||||||
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
|
||||||
for listener in listeners.drain(..).rev() {
|
|
||||||
if let AttributeValue::Listener(listener) = listener {
|
|
||||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
|
||||||
cb(uievent.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !uievent.propagates.get() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
|
dioxus-html = { path = "../html", features = ["serialize", "native-bind"], version = "^0.3.0" }
|
||||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
|
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
|
||||||
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
||||||
|
|
||||||
@@ -23,12 +23,13 @@ thiserror = "1.0.30"
|
|||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
wry = { version = "0.27.2" }
|
wry = { version = "0.27.2" }
|
||||||
futures-channel = "0.3.21"
|
futures-channel = "0.3.21"
|
||||||
tokio = { version = "1.16.1", features = [
|
tokio = { version = "1.27", features = [
|
||||||
"sync",
|
"sync",
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
"rt",
|
"rt",
|
||||||
"time",
|
"time",
|
||||||
"macros",
|
"macros",
|
||||||
|
"fs",
|
||||||
], optional = true, default-features = false }
|
], optional = true, default-features = false }
|
||||||
webbrowser = "0.8.0"
|
webbrowser = "0.8.0"
|
||||||
infer = "0.11.0"
|
infer = "0.11.0"
|
||||||
@@ -36,6 +37,7 @@ dunce = "1.0.2"
|
|||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
|
|
||||||
futures-util = "0.3.25"
|
futures-util = "0.3.25"
|
||||||
|
rfd = "0.11.3"
|
||||||
|
|
||||||
[target.'cfg(target_os = "ios")'.dependencies]
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
objc = "0.2.7"
|
objc = "0.2.7"
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
use std::{path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct FileDiologRequest {
|
||||||
|
accept: String,
|
||||||
|
multiple: bool,
|
||||||
|
pub event: String,
|
||||||
|
pub target: usize,
|
||||||
|
pub bubbles: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_file_event(request: &FileDiologRequest) -> Vec<PathBuf> {
|
||||||
|
let mut dialog = rfd::FileDialog::new();
|
||||||
|
|
||||||
|
let filters: Vec<_> = request
|
||||||
|
.accept
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|s| Filters::from_str(s).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let file_extensions: Vec<_> = filters
|
||||||
|
.iter()
|
||||||
|
.flat_map(|f| f.as_extensions().into_iter())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
dialog = dialog.add_filter("name", file_extensions.as_slice());
|
||||||
|
|
||||||
|
let files: Vec<_> = if request.multiple {
|
||||||
|
dialog.pick_files().into_iter().flatten().collect()
|
||||||
|
} else {
|
||||||
|
dialog.pick_file().into_iter().collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
files
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Filters {
|
||||||
|
Extension(String),
|
||||||
|
Mime(String),
|
||||||
|
Audio,
|
||||||
|
Video,
|
||||||
|
Image,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filters {
|
||||||
|
fn as_extensions(&self) -> Vec<&str> {
|
||||||
|
match self {
|
||||||
|
Filters::Extension(extension) => vec![extension.as_str()],
|
||||||
|
Filters::Mime(_) => vec![],
|
||||||
|
Filters::Audio => vec!["mp3", "wav", "ogg"],
|
||||||
|
Filters::Video => vec!["mp4", "webm"],
|
||||||
|
Filters::Image => vec!["png", "jpg", "jpeg", "gif", "webp"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Filters {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Some(extension) = s.strip_prefix('.') {
|
||||||
|
Ok(Filters::Extension(extension.to_string()))
|
||||||
|
} else {
|
||||||
|
match s {
|
||||||
|
"audio/*" => Ok(Filters::Audio),
|
||||||
|
"video/*" => Ok(Filters::Video),
|
||||||
|
"image/*" => Ok(Filters::Image),
|
||||||
|
_ => Ok(Filters::Mime(s.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ mod desktop_context;
|
|||||||
mod escape;
|
mod escape;
|
||||||
mod eval;
|
mod eval;
|
||||||
mod events;
|
mod events;
|
||||||
|
mod file_upload;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
mod shortcut;
|
mod shortcut;
|
||||||
mod waker;
|
mod waker;
|
||||||
@@ -19,14 +20,14 @@ pub use desktop_context::{
|
|||||||
};
|
};
|
||||||
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
||||||
use dioxus_core::*;
|
use dioxus_core::*;
|
||||||
use dioxus_html::HtmlEvent;
|
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
||||||
pub use eval::{use_eval, EvalResult};
|
pub use eval::{use_eval, EvalResult};
|
||||||
use futures_util::{pin_mut, FutureExt};
|
use futures_util::{pin_mut, FutureExt};
|
||||||
use shortcut::ShortcutRegistry;
|
use shortcut::ShortcutRegistry;
|
||||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::task::Waker;
|
use std::task::Waker;
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
pub use tao::dpi::{LogicalSize, PhysicalSize};
|
pub use tao::dpi::{LogicalSize, PhysicalSize};
|
||||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||||
pub use tao::window::WindowBuilder;
|
pub use tao::window::WindowBuilder;
|
||||||
@@ -264,6 +265,34 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EventData::Ipc(msg) if msg.method() == "file_diolog" => {
|
||||||
|
if let Ok(file_diolog) =
|
||||||
|
serde_json::from_value::<file_upload::FileDiologRequest>(msg.params())
|
||||||
|
{
|
||||||
|
let id = ElementId(file_diolog.target);
|
||||||
|
let event_name = &file_diolog.event;
|
||||||
|
let event_bubbles = file_diolog.bubbles;
|
||||||
|
let files = file_upload::get_file_event(&file_diolog);
|
||||||
|
let data = Rc::new(FormData {
|
||||||
|
value: Default::default(),
|
||||||
|
values: Default::default(),
|
||||||
|
files: Some(Arc::new(NativeFileEngine::new(files))),
|
||||||
|
});
|
||||||
|
|
||||||
|
let view = webviews.get_mut(&event.1).unwrap();
|
||||||
|
|
||||||
|
if event_name == "change&input" {
|
||||||
|
view.dom
|
||||||
|
.handle_event("input", data.clone(), id, event_bubbles);
|
||||||
|
view.dom.handle_event("change", data, id, event_bubbles);
|
||||||
|
} else {
|
||||||
|
view.dom.handle_event(event_name, data, id, event_bubbles);
|
||||||
|
}
|
||||||
|
|
||||||
|
send_edits(view.dom.render_immediate(), &view.webview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
|
Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
|
||||||
|
|||||||
@@ -9,10 +9,36 @@ use wry::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn module_loader(root_name: &str) -> String {
|
fn module_loader(root_name: &str) -> String {
|
||||||
|
let js = INTERPRETER_JS.replace(
|
||||||
|
"/*POST_HANDLE_EDITS*/",
|
||||||
|
r#"// Prevent file inputs from opening the file dialog on click
|
||||||
|
let inputs = document.querySelectorAll("input");
|
||||||
|
for (let input of inputs) {
|
||||||
|
if (!input.getAttribute("data-dioxus-file-listener")) {
|
||||||
|
input.setAttribute("data-dioxus-file-listener", true);
|
||||||
|
input.addEventListener("click", (event) => {
|
||||||
|
let target = event.target;
|
||||||
|
// prevent file inputs from opening the file dialog on click
|
||||||
|
const type = target.getAttribute("type");
|
||||||
|
if (type === "file") {
|
||||||
|
let target_id = find_real_id(target);
|
||||||
|
if (target_id !== null) {
|
||||||
|
const send = (event_name) => {
|
||||||
|
const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
|
||||||
|
window.ipc.postMessage(message);
|
||||||
|
};
|
||||||
|
send("change&input");
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
format!(
|
format!(
|
||||||
r#"
|
r#"
|
||||||
<script>
|
<script>
|
||||||
{INTERPRETER_JS}
|
{js}
|
||||||
|
|
||||||
let rootname = "{root_name}";
|
let rootname = "{root_name}";
|
||||||
let root = window.document.getElementById(rootname);
|
let root = window.document.getElementById(rootname);
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ enumset = "1.0.11"
|
|||||||
keyboard-types = "0.6.2"
|
keyboard-types = "0.6.2"
|
||||||
async-trait = "0.1.58"
|
async-trait = "0.1.58"
|
||||||
serde-value = "0.7.0"
|
serde-value = "0.7.0"
|
||||||
|
tokio = { version = "1.27", features = ["fs", "io-util"], optional = true }
|
||||||
|
rfd = { version = "0.11.3", optional = true }
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
optional = true
|
optional = true
|
||||||
@@ -48,4 +50,5 @@ serde_json = "1"
|
|||||||
default = ["serialize"]
|
default = ["serialize"]
|
||||||
serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"]
|
serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"]
|
||||||
wasm-bind = ["web-sys", "wasm-bindgen"]
|
wasm-bind = ["web-sys", "wasm-bindgen"]
|
||||||
|
native-bind = ["tokio", "rfd"]
|
||||||
hot-reload-context = ["dioxus-rsx"]
|
hot-reload-context = ["dioxus-rsx"]
|
||||||
@@ -10,12 +10,61 @@ pub type FormEvent = Event<FormData>;
|
|||||||
pub struct FormData {
|
pub struct FormData {
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
|
||||||
pub values: HashMap<String, String>,
|
pub values: HashMap<String, Vec<String>>,
|
||||||
|
|
||||||
#[cfg_attr(feature = "serialize", serde(skip))]
|
#[cfg_attr(
|
||||||
|
feature = "serialize",
|
||||||
|
serde(
|
||||||
|
default,
|
||||||
|
skip_serializing,
|
||||||
|
deserialize_with = "deserialize_file_engine"
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub files: Option<std::sync::Arc<dyn FileEngine>>,
|
pub files: Option<std::sync::Arc<dyn FileEngine>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
struct SerializedFileEngine {
|
||||||
|
files: HashMap<String, Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl FileEngine for SerializedFileEngine {
|
||||||
|
fn files(&self) -> Vec<String> {
|
||||||
|
self.files.keys().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
|
||||||
|
self.files.get(file).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_file_to_string(&self, file: &str) -> Option<String> {
|
||||||
|
self.read_file(file)
|
||||||
|
.await
|
||||||
|
.map(|bytes| String::from_utf8_lossy(&bytes).to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
fn deserialize_file_engine<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<Option<std::sync::Arc<dyn FileEngine>>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
let Ok(file_engine) =
|
||||||
|
SerializedFileEngine::deserialize(deserializer) else{
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_engine = std::sync::Arc::new(file_engine);
|
||||||
|
Ok(Some(file_engine))
|
||||||
|
}
|
||||||
|
|
||||||
impl PartialEq for FormData {
|
impl PartialEq for FormData {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.value == other.value && self.values == other.values
|
self.value == other.value && self.values == other.values
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub mod events;
|
|||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
mod global_attributes;
|
mod global_attributes;
|
||||||
pub mod input_data;
|
pub mod input_data;
|
||||||
|
#[cfg(feature = "native-bind")]
|
||||||
|
pub mod native_bind;
|
||||||
mod render_template;
|
mod render_template;
|
||||||
#[cfg(feature = "wasm-bind")]
|
#[cfg(feature = "wasm-bind")]
|
||||||
mod web_sys_bind;
|
mod web_sys_bind;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mod native_file_engine;
|
||||||
|
|
||||||
|
pub use native_file_engine::*;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::FileEngine;
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
pub struct NativeFileEngine {
|
||||||
|
files: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NativeFileEngine {
|
||||||
|
pub fn new(files: Vec<PathBuf>) -> Self {
|
||||||
|
Self { files }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl FileEngine for NativeFileEngine {
|
||||||
|
fn files(&self) -> Vec<String> {
|
||||||
|
self.files
|
||||||
|
.iter()
|
||||||
|
.filter_map(|f| Some(f.to_str()?.to_string()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
|
||||||
|
let mut file = File::open(file).await.ok()?;
|
||||||
|
|
||||||
|
let mut contents = Vec::new();
|
||||||
|
file.read_to_end(&mut contents).await.ok()?;
|
||||||
|
|
||||||
|
Some(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_file_to_string(&self, file: &str) -> Option<String> {
|
||||||
|
let mut file = File::open(file).await.ok()?;
|
||||||
|
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents).await.ok()?;
|
||||||
|
|
||||||
|
Some(contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,8 +17,7 @@ class ListenerMap {
|
|||||||
} else {
|
} else {
|
||||||
this.global[event_name].active++;
|
this.global[event_name].active++;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
const id = element.getAttribute("data-dioxus-id");
|
const id = element.getAttribute("data-dioxus-id");
|
||||||
if (!this.local[id]) {
|
if (!this.local[id]) {
|
||||||
this.local[id] = {};
|
this.local[id] = {};
|
||||||
@@ -32,11 +31,13 @@ class ListenerMap {
|
|||||||
if (bubbles) {
|
if (bubbles) {
|
||||||
this.global[event_name].active--;
|
this.global[event_name].active--;
|
||||||
if (this.global[event_name].active === 0) {
|
if (this.global[event_name].active === 0) {
|
||||||
this.root.removeEventListener(event_name, this.global[event_name].callback);
|
this.root.removeEventListener(
|
||||||
|
event_name,
|
||||||
|
this.global[event_name].callback
|
||||||
|
);
|
||||||
delete this.global[event_name];
|
delete this.global[event_name];
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
const id = element.getAttribute("data-dioxus-id");
|
const id = element.getAttribute("data-dioxus-id");
|
||||||
delete this.local[id][event_name];
|
delete this.local[id][event_name];
|
||||||
if (this.local[id].length === 0) {
|
if (this.local[id].length === 0) {
|
||||||
@@ -143,8 +144,7 @@ class Interpreter {
|
|||||||
SetAttribute(id, field, value, ns) {
|
SetAttribute(id, field, value, ns) {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
this.RemoveAttribute(id, field, ns);
|
this.RemoveAttribute(id, field, ns);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
const node = this.nodes[id];
|
const node = this.nodes[id];
|
||||||
this.SetAttributeInner(node, field, value, ns);
|
this.SetAttributeInner(node, field, value, ns);
|
||||||
}
|
}
|
||||||
@@ -167,10 +167,10 @@ class Interpreter {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "checked":
|
case "checked":
|
||||||
node.checked = value === "true";
|
node.checked = value === "true" || value === true;
|
||||||
break;
|
break;
|
||||||
case "selected":
|
case "selected":
|
||||||
node.selected = value === "true";
|
node.selected = value === "true" || value === true;
|
||||||
break;
|
break;
|
||||||
case "dangerous_inner_html":
|
case "dangerous_inner_html":
|
||||||
node.innerHTML = value;
|
node.innerHTML = value;
|
||||||
@@ -212,6 +212,8 @@ class Interpreter {
|
|||||||
for (let edit of edits.edits) {
|
for (let edit of edits.edits) {
|
||||||
this.handleEdit(edit);
|
this.handleEdit(edit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*POST_HANDLE_EDITS*/
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveTemplate(template) {
|
SaveTemplate(template) {
|
||||||
@@ -342,7 +344,6 @@ class Interpreter {
|
|||||||
this.RemoveEventListener(edit.id, edit.name);
|
this.RemoveEventListener(edit.id, edit.name);
|
||||||
break;
|
break;
|
||||||
case "NewEventListener":
|
case "NewEventListener":
|
||||||
|
|
||||||
let bubbles = event_bubbles(edit.name);
|
let bubbles = event_bubbles(edit.name);
|
||||||
|
|
||||||
// this handler is only provided on desktop implementations since this
|
// this handler is only provided on desktop implementations since this
|
||||||
@@ -351,7 +352,7 @@ class Interpreter {
|
|||||||
let target = event.target;
|
let target = event.target;
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
let realId = target.getAttribute(`data-dioxus-id`);
|
let realId = target.getAttribute(`data-dioxus-id`);
|
||||||
let shouldPreventDefault = target.getAttribute(
|
let preventDefaultRequests = target.getAttribute(
|
||||||
`dioxus-prevent-default`
|
`dioxus-prevent-default`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -360,7 +361,14 @@ class Interpreter {
|
|||||||
let a_element = target.closest("a");
|
let a_element = target.closest("a");
|
||||||
if (a_element != null) {
|
if (a_element != null) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (shouldPreventDefault !== `onclick` && a_element.getAttribute(`dioxus-prevent-default`) !== `onclick`) {
|
|
||||||
|
let elementShouldPreventDefault =
|
||||||
|
preventDefaultRequests.includes(`onclick`);
|
||||||
|
let linkShouldPreventDefault = a_element
|
||||||
|
.getAttribute(`dioxus-prevent-default`)
|
||||||
|
.includes(`onclick`);
|
||||||
|
|
||||||
|
if (!elementShouldPreventDefault && !linkShouldPreventDefault) {
|
||||||
const href = a_element.getAttribute("href");
|
const href = a_element.getAttribute("href");
|
||||||
if (href !== "" && href !== null && href !== undefined) {
|
if (href !== "" && href !== null && href !== undefined) {
|
||||||
window.ipc.postMessage(
|
window.ipc.postMessage(
|
||||||
@@ -386,13 +394,13 @@ class Interpreter {
|
|||||||
realId = target.getAttribute(`data-dioxus-id`);
|
realId = target.getAttribute(`data-dioxus-id`);
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldPreventDefault = target.getAttribute(
|
preventDefaultRequests = target.getAttribute(
|
||||||
`dioxus-prevent-default`
|
`dioxus-prevent-default`
|
||||||
);
|
);
|
||||||
|
|
||||||
let contents = serialize_event(event);
|
let contents = serialize_event(event);
|
||||||
|
|
||||||
if (shouldPreventDefault === `on${event.type}`) {
|
if (preventDefaultRequests.includes(`on${event.type}`)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +451,99 @@ class Interpreter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this handler is only provided on the desktop and liveview implementations since this
|
||||||
|
// method is not used by the web implementation
|
||||||
|
function handler(event, name, bubbles) {
|
||||||
|
let target = event.target;
|
||||||
|
if (target != null) {
|
||||||
|
let shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
|
||||||
|
|
||||||
|
if (event.type === "click") {
|
||||||
|
// Prevent redirects from links
|
||||||
|
let a_element = target.closest("a");
|
||||||
|
if (a_element != null) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (
|
||||||
|
shouldPreventDefault !== `onclick` &&
|
||||||
|
a_element.getAttribute(`dioxus-prevent-default`) !== `onclick`
|
||||||
|
) {
|
||||||
|
const href = a_element.getAttribute("href");
|
||||||
|
if (href !== "" && href !== null && href !== undefined) {
|
||||||
|
window.ipc.postMessage(
|
||||||
|
serializeIpcMessage("browser_open", { href })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// also prevent buttons from submitting
|
||||||
|
if (target.tagName === "BUTTON" && event.type == "submit") {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const realId = find_real_id(target);
|
||||||
|
|
||||||
|
shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
|
||||||
|
|
||||||
|
if (shouldPreventDefault === `on${event.type}`) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "submit") {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = serialize_event(event);
|
||||||
|
|
||||||
|
/*POST_EVENT_SERIALIZATION*/
|
||||||
|
|
||||||
|
if (
|
||||||
|
target.tagName === "FORM" &&
|
||||||
|
(event.type === "submit" || event.type === "input")
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
target.tagName === "FORM" &&
|
||||||
|
(event.type === "submit" || event.type === "input")
|
||||||
|
) {
|
||||||
|
const formData = new FormData(target);
|
||||||
|
|
||||||
|
for (let name of formData.keys()) {
|
||||||
|
let value = formData.getAll(name);
|
||||||
|
contents.values[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.ipc.postMessage(
|
||||||
|
serializeIpcMessage("user_event", {
|
||||||
|
name: name,
|
||||||
|
element: parseInt(realId),
|
||||||
|
data: contents,
|
||||||
|
bubbles,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function find_real_id(target) {
|
||||||
|
let realId = target.getAttribute(`data-dioxus-id`);
|
||||||
|
// walk the tree to find the real element
|
||||||
|
while (realId == null) {
|
||||||
|
// we've reached the root we don't want to send an event
|
||||||
|
if (target.parentElement === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target = target.parentElement;
|
||||||
|
realId = target.getAttribute(`data-dioxus-id`);
|
||||||
|
}
|
||||||
|
return realId;
|
||||||
|
}
|
||||||
|
|
||||||
function get_mouse_data(event) {
|
function get_mouse_data(event) {
|
||||||
const {
|
const {
|
||||||
altKey,
|
altKey,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ futures-util = { version = "0.3.25", default-features = false, features = [
|
|||||||
"sink",
|
"sink",
|
||||||
] }
|
] }
|
||||||
futures-channel = { version = "0.3.25", features = ["sink"] }
|
futures-channel = { version = "0.3.25", features = ["sink"] }
|
||||||
tokio = { version = "1.22.0", features = ["time"] }
|
tokio = { version = "1.22.0", features = ["time", "macros"] }
|
||||||
tokio-stream = { version = "0.1.11", features = ["net"] }
|
tokio-stream = { version = "0.1.11", features = ["net"] }
|
||||||
tokio-util = { version = "0.7.4", features = ["rt"] }
|
tokio-util = { version = "0.7.4", features = ["rt"] }
|
||||||
serde = { version = "1.0.151", features = ["derive"] }
|
serde = { version = "1.0.151", features = ["derive"] }
|
||||||
@@ -36,6 +36,7 @@ axum = { version = "0.6.1", optional = true, features = ["ws"] }
|
|||||||
|
|
||||||
# salvo
|
# salvo
|
||||||
salvo = { version = "0.37.7", optional = true, features = ["ws"] }
|
salvo = { version = "0.37.7", optional = true, features = ["ws"] }
|
||||||
|
once_cell = "1.17.1"
|
||||||
|
|
||||||
# actix is ... complicated?
|
# actix is ... complicated?
|
||||||
# actix-files = { version = "0.6.2", optional = true }
|
# actix-files = { version = "0.6.2", optional = true }
|
||||||
|
|||||||
@@ -34,7 +34,51 @@ pub enum LiveViewError {
|
|||||||
SendingFailed,
|
SendingFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
use dioxus_interpreter_js::INTERPRETER_JS;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
static INTERPRETER_JS: Lazy<String> = Lazy::new(|| {
|
||||||
|
let interpreter = dioxus_interpreter_js::INTERPRETER_JS;
|
||||||
|
let serialize_file_uploads = r#"if (
|
||||||
|
target.tagName === "INPUT" &&
|
||||||
|
(event.type === "change" || event.type === "input")
|
||||||
|
) {
|
||||||
|
const type = target.getAttribute("type");
|
||||||
|
if (type === "file") {
|
||||||
|
async function read_files() {
|
||||||
|
const files = target.files;
|
||||||
|
const file_contents = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
|
||||||
|
file_contents[file.name] = Array.from(
|
||||||
|
new Uint8Array(await file.arrayBuffer())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let file_engine = {
|
||||||
|
files: file_contents,
|
||||||
|
};
|
||||||
|
contents.files = file_engine;
|
||||||
|
|
||||||
|
if (realId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = serializeIpcMessage("user_event", {
|
||||||
|
name: name,
|
||||||
|
element: parseInt(realId),
|
||||||
|
data: contents,
|
||||||
|
bubbles,
|
||||||
|
});
|
||||||
|
window.ipc.postMessage(message);
|
||||||
|
}
|
||||||
|
read_files();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads)
|
||||||
|
});
|
||||||
|
|
||||||
static MAIN_JS: &str = include_str!("./main.js");
|
static MAIN_JS: &str = include_str!("./main.js");
|
||||||
|
|
||||||
/// This script that gets injected into your app connects this page to the websocket endpoint
|
/// This script that gets injected into your app connects this page to the websocket endpoint
|
||||||
@@ -42,11 +86,12 @@ static MAIN_JS: &str = include_str!("./main.js");
|
|||||||
/// Once the endpoint is connected, it will send the initial state of the app, and then start
|
/// Once the endpoint is connected, it will send the initial state of the app, and then start
|
||||||
/// processing user events and returning edits to the liveview instance
|
/// processing user events and returning edits to the liveview instance
|
||||||
pub fn interpreter_glue(url: &str) -> String {
|
pub fn interpreter_glue(url: &str) -> String {
|
||||||
|
let js = &*INTERPRETER_JS;
|
||||||
format!(
|
format!(
|
||||||
r#"
|
r#"
|
||||||
<script>
|
<script>
|
||||||
var WS_ADDR = "{url}";
|
var WS_ADDR = "{url}";
|
||||||
{INTERPRETER_JS}
|
{js}
|
||||||
{MAIN_JS}
|
{MAIN_JS}
|
||||||
main();
|
main();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ lightningcss = "1.0.0-alpha.39"
|
|||||||
|
|
||||||
rayon = "1.6.1"
|
rayon = "1.6.1"
|
||||||
shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false }
|
shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false }
|
||||||
shipyard_hierarchy = "0.6.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ pub fn render<R: Driver>(
|
|||||||
let event_tx_clone = event_tx.clone();
|
let event_tx_clone = event_tx.clone();
|
||||||
if !cfg.headless {
|
if !cfg.headless {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let tick_rate = Duration::from_millis(1000);
|
// Timeout after 10ms when waiting for events
|
||||||
|
let tick_rate = Duration::from_millis(10);
|
||||||
loop {
|
loop {
|
||||||
if crossterm::event::poll(tick_rate).unwrap() {
|
if crossterm::event::poll(tick_rate).unwrap() {
|
||||||
let evt = crossterm::event::read().unwrap();
|
let evt = crossterm::event::read().unwrap();
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
|
|||||||
router_root.register_total_route(route_context.total_route, cx.scope_id());
|
router_root.register_total_route(route_context.total_route, cx.scope_id());
|
||||||
});
|
});
|
||||||
|
|
||||||
log::debug!("Checking Route: {:?}", cx.props.to);
|
log::trace!("Checking Route: {:?}", cx.props.to);
|
||||||
|
|
||||||
if router_root.should_render(cx.scope_id()) {
|
if router_root.should_render(cx.scope_id()) {
|
||||||
log::debug!("Route should render: {:?}", cx.scope_id());
|
log::trace!("Route should render: {:?}", cx.scope_id());
|
||||||
cx.render(rsx!(&cx.props.children))
|
cx.render(rsx!(&cx.props.children))
|
||||||
} else {
|
} else {
|
||||||
log::debug!("Route should *not* render: {:?}", cx.scope_id());
|
log::trace!("Route should *not* render: {:?}", cx.scope_id());
|
||||||
cx.render(rsx!(()))
|
cx.render(rsx!(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,3 +25,6 @@ convert_case = "0.5.0"
|
|||||||
# default = ["html"]
|
# default = ["html"]
|
||||||
|
|
||||||
# eventually more output options
|
# eventually more output options
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "1.2.1"
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use html_parser::Dom;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_elements() {
|
||||||
|
let html = r#"
|
||||||
|
<div>
|
||||||
|
<div class="asd">hello world!</div>
|
||||||
|
<div id="asd">hello world!</div>
|
||||||
|
<div id="asd">hello world!</div>
|
||||||
|
<div for="asd">hello world!</div>
|
||||||
|
<div async="asd">hello world!</div>
|
||||||
|
<div LargeThing="asd">hello world!</div>
|
||||||
|
<ai-is-awesome>hello world!</ai-is-awesome>
|
||||||
|
</div>
|
||||||
|
"#
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
let dom = Dom::parse(html).unwrap();
|
||||||
|
|
||||||
|
let body = rsx_rosetta::rsx_from_html(&dom);
|
||||||
|
|
||||||
|
let out = dioxus_autofmt::write_block_out(body).unwrap();
|
||||||
|
|
||||||
|
let expected = r#"
|
||||||
|
div {
|
||||||
|
div { class: "asd", "hello world!" }
|
||||||
|
div { id: "asd", "hello world!" }
|
||||||
|
div { id: "asd", "hello world!" }
|
||||||
|
div { r#for: "asd", "hello world!" }
|
||||||
|
div { r#async: "asd", "hello world!" }
|
||||||
|
div { large_thing: "asd", "hello world!" }
|
||||||
|
ai_is_awesome { "hello world!" }
|
||||||
|
}"#;
|
||||||
|
pretty_assertions::assert_eq!(&out, &expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deeply_nested() {
|
||||||
|
let html = r#"
|
||||||
|
<div>
|
||||||
|
<div class="asd">
|
||||||
|
<div class="asd">
|
||||||
|
<div class="asd">
|
||||||
|
<div class="asd">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"#
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
let dom = Dom::parse(html).unwrap();
|
||||||
|
|
||||||
|
let body = rsx_rosetta::rsx_from_html(&dom);
|
||||||
|
|
||||||
|
let out = dioxus_autofmt::write_block_out(body).unwrap();
|
||||||
|
|
||||||
|
let expected = r#"
|
||||||
|
div {
|
||||||
|
div { class: "asd",
|
||||||
|
div { class: "asd",
|
||||||
|
div { class: "asd", div { class: "asd" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
pretty_assertions::assert_eq!(&out, &expected);
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ futures-channel = "0.3.21"
|
|||||||
serde_json = { version = "1.0" }
|
serde_json = { version = "1.0" }
|
||||||
serde = { version = "1.0" }
|
serde = { version = "1.0" }
|
||||||
serde-wasm-bindgen = "0.4.5"
|
serde-wasm-bindgen = "0.4.5"
|
||||||
|
async-trait = "0.1.58"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3.56"
|
version = "0.3.56"
|
||||||
@@ -76,6 +77,9 @@ features = [
|
|||||||
"Location",
|
"Location",
|
||||||
"MessageEvent",
|
"MessageEvent",
|
||||||
"console",
|
"console",
|
||||||
|
"FileList",
|
||||||
|
"File",
|
||||||
|
"FileReader"
|
||||||
]
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
+52
-39
@@ -10,15 +10,16 @@
|
|||||||
use dioxus_core::{
|
use dioxus_core::{
|
||||||
BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
|
BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
|
||||||
};
|
};
|
||||||
use dioxus_html::{event_bubbles, CompositionData, FormData};
|
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData};
|
||||||
use dioxus_interpreter_js::{save_template, Channel};
|
use dioxus_interpreter_js::{save_template, Channel};
|
||||||
use futures_channel::mpsc;
|
use futures_channel::mpsc;
|
||||||
|
use js_sys::Array;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::{any::Any, rc::Rc};
|
use std::{any::Any, rc::Rc, sync::Arc};
|
||||||
use wasm_bindgen::{closure::Closure, JsCast};
|
use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast};
|
||||||
use web_sys::{Document, Element, Event, HtmlElement};
|
use web_sys::{console, Document, Element, Event, HtmlElement};
|
||||||
|
|
||||||
use crate::Config;
|
use crate::{file_engine::WebFileEngine, Config};
|
||||||
|
|
||||||
pub struct WebsysDom {
|
pub struct WebsysDom {
|
||||||
document: Document,
|
document: Document,
|
||||||
@@ -54,13 +55,17 @@ impl WebsysDom {
|
|||||||
let element = walk_event_for_id(event);
|
let element = walk_event_for_id(event);
|
||||||
let bubbles = dioxus_html::event_bubbles(name.as_str());
|
let bubbles = dioxus_html::event_bubbles(name.as_str());
|
||||||
if let Some((element, target)) = element {
|
if let Some((element, target)) = element {
|
||||||
if target
|
if let Some(prevent_requests) = target
|
||||||
.get_attribute("dioxus-prevent-default")
|
.get_attribute("dioxus-prevent-default")
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|f| f.trim_start_matches("on"))
|
.map(|f| f.split_whitespace())
|
||||||
== Some(&name)
|
|
||||||
{
|
{
|
||||||
event.prevent_default();
|
if prevent_requests
|
||||||
|
.map(|f| f.trim_start_matches("on"))
|
||||||
|
.any(|f| f == name)
|
||||||
|
{
|
||||||
|
event.prevent_default();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = virtual_event_from_websys_event(event.clone(), target);
|
let data = virtual_event_from_websys_event(event.clone(), target);
|
||||||
@@ -206,6 +211,7 @@ impl WebsysDom {
|
|||||||
},
|
},
|
||||||
SetText { value, id } => i.set_text(id.0 as u32, value),
|
SetText { value, id } => i.set_text(id.0 as u32, value),
|
||||||
NewEventListener { name, id, .. } => {
|
NewEventListener { name, id, .. } => {
|
||||||
|
console::log_1(&format!("new event listener: {}", name).into());
|
||||||
i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
|
i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
|
||||||
}
|
}
|
||||||
RemoveEventListener { name, id } => {
|
RemoveEventListener { name, id } => {
|
||||||
@@ -224,6 +230,7 @@ impl WebsysDom {
|
|||||||
// We need tests that simulate clicks/etc and make sure every event type works.
|
// We need tests that simulate clicks/etc and make sure every event type works.
|
||||||
pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc<dyn Any> {
|
pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc<dyn Any> {
|
||||||
use dioxus_html::events::*;
|
use dioxus_html::events::*;
|
||||||
|
console::log_1(&event.clone().into());
|
||||||
|
|
||||||
match event.type_().as_str() {
|
match event.type_().as_str() {
|
||||||
"copy" | "cut" | "paste" => Rc::new(ClipboardData {}),
|
"copy" | "cut" | "paste" => Rc::new(ClipboardData {}),
|
||||||
@@ -325,47 +332,53 @@ fn read_input_to_data(target: Element) -> Rc<FormData> {
|
|||||||
|
|
||||||
// try to fill in form values
|
// try to fill in form values
|
||||||
if let Some(form) = target.dyn_ref::<web_sys::HtmlFormElement>() {
|
if let Some(form) = target.dyn_ref::<web_sys::HtmlFormElement>() {
|
||||||
let elements = form.elements();
|
let form_data = get_form_data(form);
|
||||||
for x in 0..elements.length() {
|
for value in form_data.entries().into_iter().flatten() {
|
||||||
let element = elements.item(x).unwrap();
|
if let Ok(array) = value.dyn_into::<Array>() {
|
||||||
if let Some(name) = element.get_attribute("name") {
|
if let Some(name) = array.get(0).as_string() {
|
||||||
let value: Option<String> = element
|
if let Ok(item_values) = array.get(1).dyn_into::<Array>() {
|
||||||
.dyn_ref()
|
let item_values =
|
||||||
.map(|input: &web_sys::HtmlInputElement| {
|
item_values.iter().filter_map(|v| v.as_string()).collect();
|
||||||
match input.type_().as_str() {
|
|
||||||
"checkbox" => {
|
values.insert(name, item_values);
|
||||||
match input.checked() {
|
}
|
||||||
true => Some("true".to_string()),
|
|
||||||
false => Some("false".to_string()),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"radio" => {
|
|
||||||
match input.checked() {
|
|
||||||
true => Some(input.value()),
|
|
||||||
false => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Some(input.value())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value())))
|
|
||||||
.or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value())))
|
|
||||||
.or_else(|| Some(element.dyn_ref::<web_sys::HtmlElement>().unwrap().text_content()))
|
|
||||||
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
|
|
||||||
if let Some(value) = value {
|
|
||||||
values.insert(name, value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let files = target
|
||||||
|
.dyn_ref()
|
||||||
|
.and_then(|input: &web_sys::HtmlInputElement| {
|
||||||
|
input.files().and_then(|files| {
|
||||||
|
WebFileEngine::new(files).map(|f| Arc::new(f) as Arc<dyn FileEngine>)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
Rc::new(FormData {
|
Rc::new(FormData {
|
||||||
value,
|
value,
|
||||||
values,
|
values,
|
||||||
files: None,
|
files,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// web-sys does not expose the keys api for form data, so we need to manually bind to it
|
||||||
|
#[wasm_bindgen(inline_js = r#"
|
||||||
|
export function get_form_data(form) {
|
||||||
|
let values = new Map();
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
for (let name of formData.keys()) {
|
||||||
|
values.set(name, formData.getAll(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
"#)]
|
||||||
|
extern "C" {
|
||||||
|
fn get_form_data(form: &web_sys::HtmlFormElement) -> js_sys::Map;
|
||||||
|
}
|
||||||
|
|
||||||
fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> {
|
fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> {
|
||||||
let mut target = event
|
let mut target = event
|
||||||
.target()
|
.target()
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
use dioxus_html::FileEngine;
|
||||||
|
use futures_channel::oneshot;
|
||||||
|
use js_sys::Uint8Array;
|
||||||
|
use wasm_bindgen::{prelude::Closure, JsCast};
|
||||||
|
use web_sys::{File, FileList, FileReader};
|
||||||
|
|
||||||
|
pub(crate) struct WebFileEngine {
|
||||||
|
file_reader: FileReader,
|
||||||
|
file_list: FileList,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebFileEngine {
|
||||||
|
pub fn new(file_list: FileList) -> Option<Self> {
|
||||||
|
Some(Self {
|
||||||
|
file_list,
|
||||||
|
file_reader: FileReader::new().ok()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.file_list.length() as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, index: usize) -> Option<File> {
|
||||||
|
self.file_list.item(index as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find(&self, name: &str) -> Option<File> {
|
||||||
|
(0..self.len())
|
||||||
|
.filter_map(|i| self.get(i))
|
||||||
|
.find(|f| f.name() == name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl FileEngine for WebFileEngine {
|
||||||
|
fn files(&self) -> Vec<String> {
|
||||||
|
(0..self.len())
|
||||||
|
.filter_map(|i| self.get(i).map(|f| f.name()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a file to bytes
|
||||||
|
async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
|
||||||
|
let file = self.find(file)?;
|
||||||
|
|
||||||
|
let file_reader = self.file_reader.clone();
|
||||||
|
let (rx, tx) = oneshot::channel();
|
||||||
|
let on_load: Closure<dyn FnMut()> = Closure::new({
|
||||||
|
let mut rx = Some(rx);
|
||||||
|
move || {
|
||||||
|
let result = file_reader.result();
|
||||||
|
let _ = rx
|
||||||
|
.take()
|
||||||
|
.expect("multiple files read without refreshing the channel")
|
||||||
|
.send(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.file_reader
|
||||||
|
.set_onload(Some(on_load.as_ref().unchecked_ref()));
|
||||||
|
on_load.forget();
|
||||||
|
self.file_reader.read_as_array_buffer(&file).ok()?;
|
||||||
|
|
||||||
|
if let Ok(Ok(js_val)) = tx.await {
|
||||||
|
let as_u8_arr = Uint8Array::new(&js_val);
|
||||||
|
let as_u8_vec = as_u8_arr.to_vec();
|
||||||
|
|
||||||
|
Some(as_u8_vec)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a file to string
|
||||||
|
async fn read_file_to_string(&self, file: &str) -> Option<String> {
|
||||||
|
let file = self.find(file)?;
|
||||||
|
|
||||||
|
let file_reader = self.file_reader.clone();
|
||||||
|
let (rx, tx) = oneshot::channel();
|
||||||
|
let on_load: Closure<dyn FnMut()> = Closure::new({
|
||||||
|
let mut rx = Some(rx);
|
||||||
|
move || {
|
||||||
|
let result = file_reader.result();
|
||||||
|
let _ = rx
|
||||||
|
.take()
|
||||||
|
.expect("multiple files read without refreshing the channel")
|
||||||
|
.send(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.file_reader
|
||||||
|
.set_onload(Some(on_load.as_ref().unchecked_ref()));
|
||||||
|
on_load.forget();
|
||||||
|
self.file_reader.read_as_text(&file).ok()?;
|
||||||
|
|
||||||
|
if let Ok(Ok(js_val)) = tx.await {
|
||||||
|
js_val.as_string()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@ use futures_util::{pin_mut, FutureExt, StreamExt};
|
|||||||
mod cache;
|
mod cache;
|
||||||
mod cfg;
|
mod cfg;
|
||||||
mod dom;
|
mod dom;
|
||||||
|
mod file_engine;
|
||||||
mod hot_reload;
|
mod hot_reload;
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
mod rehydrate;
|
mod rehydrate;
|
||||||
@@ -225,7 +226,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
|
|||||||
websys_dom.mount();
|
websys_dom.mount();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
log::debug!("waiting for work");
|
log::trace!("waiting for work");
|
||||||
|
|
||||||
// if virtualdom has nothing, wait for it to have something before requesting idle time
|
// if virtualdom has nothing, wait for it to have something before requesting idle time
|
||||||
// if there is work then this future resolves immediately.
|
// if there is work then this future resolves immediately.
|
||||||
|
|||||||
Reference in new issue
Block a user