mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-14 00:17:17 +00:00
Merge branch 'upstream' into fix-links-liveview
This commit is contained in:
commit
7e292cc2fa
18 changed files with 628 additions and 84 deletions
37
examples/file_upload.rs
Normal file
37
examples/file_upload.rs
Normal file
|
@ -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}" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -384,51 +384,73 @@ impl VirtualDom {
|
|||
data,
|
||||
};
|
||||
|
||||
// Loop through each dynamic attribute in this template before moving up to the template's parent.
|
||||
while 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;
|
||||
// If the event bubbles, we traverse through the tree until we find the target element.
|
||||
if bubbles {
|
||||
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
|
||||
while 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];
|
||||
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
|
||||
if attr.name.trim_start_matches("on") == name
|
||||
&& target_path.is_decendant(&this_path)
|
||||
{
|
||||
listeners.push(&attr.value);
|
||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||
if attr.name.trim_start_matches("on") == name
|
||||
&& target_path.is_decendant(&this_path)
|
||||
{
|
||||
listeners.push(&attr.value);
|
||||
|
||||
// Break if the event doesn't bubble anyways
|
||||
if !bubbles {
|
||||
break;
|
||||
// Break if this is the exact target element.
|
||||
// This means we won't call two listeners with the same name on the same element. This should be
|
||||
// documented, or be rejected from the rsx! macro outright
|
||||
if target_path == this_path {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Break if this is the exact target element.
|
||||
// This means we won't call two listeners with the same name on the same element. This should be
|
||||
// documented, or be rejected from the rsx! macro outright
|
||||
if target_path == this_path {
|
||||
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));
|
||||
}
|
||||
} 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]
|
||||
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-hot-reload = { path = "../hot-reload", optional = true }
|
||||
|
||||
|
@ -23,12 +23,13 @@ thiserror = "1.0.30"
|
|||
log = "0.4.14"
|
||||
wry = { version = "0.27.2" }
|
||||
futures-channel = "0.3.21"
|
||||
tokio = { version = "1.16.1", features = [
|
||||
tokio = { version = "1.27", features = [
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"rt",
|
||||
"time",
|
||||
"macros",
|
||||
"fs",
|
||||
], optional = true, default-features = false }
|
||||
webbrowser = "0.8.0"
|
||||
infer = "0.11.0"
|
||||
|
@ -36,6 +37,7 @@ dunce = "1.0.2"
|
|||
slab = "0.4"
|
||||
|
||||
futures-util = "0.3.25"
|
||||
rfd = "0.11.3"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
|
|
74
packages/desktop/src/file_upload.rs
Normal file
74
packages/desktop/src/file_upload.rs
Normal file
|
@ -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 eval;
|
||||
mod events;
|
||||
mod file_upload;
|
||||
mod protocol;
|
||||
mod shortcut;
|
||||
mod waker;
|
||||
|
@ -19,14 +20,14 @@ pub use desktop_context::{
|
|||
};
|
||||
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
||||
use dioxus_core::*;
|
||||
use dioxus_html::HtmlEvent;
|
||||
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
||||
pub use eval::{use_eval, EvalResult};
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use shortcut::ShortcutRegistry;
|
||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::task::Waker;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
pub use tao::dpi::{LogicalSize, PhysicalSize};
|
||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||
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),
|
||||
|
|
|
@ -9,10 +9,36 @@ use wry::{
|
|||
};
|
||||
|
||||
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!(
|
||||
r#"
|
||||
<script>
|
||||
{INTERPRETER_JS}
|
||||
{js}
|
||||
|
||||
let rootname = "{root_name}";
|
||||
let root = window.document.getElementById(rootname);
|
||||
|
|
|
@ -21,6 +21,8 @@ enumset = "1.0.11"
|
|||
keyboard-types = "0.6.2"
|
||||
async-trait = "0.1.58"
|
||||
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]
|
||||
optional = true
|
||||
|
@ -48,4 +50,5 @@ serde_json = "1"
|
|||
default = ["serialize"]
|
||||
serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"]
|
||||
wasm-bind = ["web-sys", "wasm-bindgen"]
|
||||
native-bind = ["tokio", "rfd"]
|
||||
hot-reload-context = ["dioxus-rsx"]
|
||||
|
|
|
@ -10,12 +10,57 @@ pub type FormEvent = Event<FormData>;
|
|||
pub struct FormData {
|
||||
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(skip_serializing, deserialize_with = "deserialize_file_engine")
|
||||
)]
|
||||
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 {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.value == other.value && self.values == other.values
|
||||
|
|
|
@ -20,6 +20,8 @@ pub mod events;
|
|||
pub mod geometry;
|
||||
mod global_attributes;
|
||||
pub mod input_data;
|
||||
#[cfg(feature = "native-bind")]
|
||||
pub mod native_bind;
|
||||
mod render_template;
|
||||
#[cfg(feature = "wasm-bind")]
|
||||
mod web_sys_bind;
|
||||
|
|
3
packages/html/src/native_bind/mod.rs
Normal file
3
packages/html/src/native_bind/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod native_file_engine;
|
||||
|
||||
pub use native_file_engine::*;
|
43
packages/html/src/native_bind/native_file_engine.rs
Normal file
43
packages/html/src/native_bind/native_file_engine.rs
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -174,10 +174,10 @@ class Interpreter {
|
|||
}
|
||||
break;
|
||||
case "checked":
|
||||
node.checked = value === "true";
|
||||
node.checked = value === "true" || value === true;
|
||||
break;
|
||||
case "selected":
|
||||
node.selected = value === "true";
|
||||
node.selected = value === "true" || value === true;
|
||||
break;
|
||||
case "dangerous_inner_html":
|
||||
node.innerHTML = value;
|
||||
|
@ -219,6 +219,8 @@ class Interpreter {
|
|||
for (let edit of edits.edits) {
|
||||
this.handleEdit(edit);
|
||||
}
|
||||
|
||||
/*POST_HANDLE_EDITS*/
|
||||
}
|
||||
|
||||
SaveTemplate(template) {
|
||||
|
@ -455,6 +457,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) {
|
||||
const {
|
||||
altKey,
|
||||
|
|
|
@ -18,7 +18,7 @@ futures-util = { version = "0.3.25", default-features = false, 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-util = { version = "0.7.4", features = ["rt"] }
|
||||
serde = { version = "1.0.151", features = ["derive"] }
|
||||
|
@ -36,6 +36,7 @@ axum = { version = "0.6.1", optional = true, features = ["ws"] }
|
|||
|
||||
# salvo
|
||||
salvo = { version = "0.37.7", optional = true, features = ["ws"] }
|
||||
once_cell = "1.17.1"
|
||||
|
||||
# actix is ... complicated?
|
||||
# actix-files = { version = "0.6.2", optional = true }
|
||||
|
|
|
@ -34,7 +34,51 @@ pub enum LiveViewError {
|
|||
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");
|
||||
|
||||
/// 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
|
||||
/// processing user events and returning edits to the liveview instance
|
||||
pub fn interpreter_glue(url: &str) -> String {
|
||||
let js = &*INTERPRETER_JS;
|
||||
format!(
|
||||
r#"
|
||||
<script>
|
||||
var WS_ADDR = "{url}";
|
||||
{INTERPRETER_JS}
|
||||
{js}
|
||||
{MAIN_JS}
|
||||
main();
|
||||
</script>
|
||||
|
|
|
@ -32,6 +32,7 @@ futures-channel = "0.3.21"
|
|||
serde_json = { version = "1.0" }
|
||||
serde = { version = "1.0" }
|
||||
serde-wasm-bindgen = "0.4.5"
|
||||
async-trait = "0.1.58"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.56"
|
||||
|
@ -76,6 +77,9 @@ features = [
|
|||
"Location",
|
||||
"MessageEvent",
|
||||
"console",
|
||||
"FileList",
|
||||
"File",
|
||||
"FileReader"
|
||||
]
|
||||
|
||||
[features]
|
||||
|
|
|
@ -10,15 +10,16 @@
|
|||
use dioxus_core::{
|
||||
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 futures_channel::mpsc;
|
||||
use js_sys::Array;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{any::Any, rc::Rc};
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
use web_sys::{Document, Element, Event, HtmlElement};
|
||||
use std::{any::Any, rc::Rc, sync::Arc};
|
||||
use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast};
|
||||
use web_sys::{console, Document, Element, Event, HtmlElement};
|
||||
|
||||
use crate::Config;
|
||||
use crate::{file_engine::WebFileEngine, Config};
|
||||
|
||||
pub struct WebsysDom {
|
||||
document: Document,
|
||||
|
@ -206,6 +207,7 @@ impl WebsysDom {
|
|||
},
|
||||
SetText { value, id } => i.set_text(id.0 as u32, value),
|
||||
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);
|
||||
}
|
||||
RemoveEventListener { name, id } => {
|
||||
|
@ -224,6 +226,7 @@ impl WebsysDom {
|
|||
// 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> {
|
||||
use dioxus_html::events::*;
|
||||
console::log_1(&event.clone().into());
|
||||
|
||||
match event.type_().as_str() {
|
||||
"copy" | "cut" | "paste" => Rc::new(ClipboardData {}),
|
||||
|
@ -325,47 +328,53 @@ fn read_input_to_data(target: Element) -> Rc<FormData> {
|
|||
|
||||
// try to fill in form values
|
||||
if let Some(form) = target.dyn_ref::<web_sys::HtmlFormElement>() {
|
||||
let elements = form.elements();
|
||||
for x in 0..elements.length() {
|
||||
let element = elements.item(x).unwrap();
|
||||
if let Some(name) = element.get_attribute("name") {
|
||||
let value: Option<String> = element
|
||||
.dyn_ref()
|
||||
.map(|input: &web_sys::HtmlInputElement| {
|
||||
match input.type_().as_str() {
|
||||
"checkbox" => {
|
||||
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 form_data = get_form_data(form);
|
||||
for value in form_data.entries().into_iter().flatten() {
|
||||
if let Ok(array) = value.dyn_into::<Array>() {
|
||||
if let Some(name) = array.get(0).as_string() {
|
||||
if let Ok(item_values) = array.get(1).dyn_into::<Array>() {
|
||||
let item_values =
|
||||
item_values.iter().filter_map(|v| v.as_string()).collect();
|
||||
|
||||
values.insert(name, item_values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
value,
|
||||
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)> {
|
||||
let mut target = event
|
||||
.target()
|
||||
|
|
103
packages/web/src/file_engine.rs
Normal file
103
packages/web/src/file_engine.rs
Normal file
|
@ -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 cfg;
|
||||
mod dom;
|
||||
mod file_engine;
|
||||
mod hot_reload;
|
||||
#[cfg(feature = "hydrate")]
|
||||
mod rehydrate;
|
||||
|
|
Loading…
Reference in a new issue