From f1f7517b888e9e77e612d656ee0c876717c799af Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 6 Apr 2023 11:11:08 -0500 Subject: [PATCH 1/9] Fix form events with select multiple --- packages/html/src/events/form.rs | 2 +- packages/interpreter/src/interpreter.js | 41 +++++++---------- packages/web/src/dom.rs | 59 ++++++++++++------------- 3 files changed, 45 insertions(+), 57 deletions(-) diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 361ebe2d4..6edf60448 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -10,7 +10,7 @@ pub type FormEvent = Event; pub struct FormData { pub value: String, - pub values: HashMap, + pub values: HashMap>, #[cfg_attr(feature = "serialize", serde(skip))] pub files: Option>, diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index b921c7c22..653eb15fb 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -17,8 +17,7 @@ class ListenerMap { } else { this.global[event_name].active++; } - } - else { + } else { const id = element.getAttribute("data-dioxus-id"); if (!this.local[id]) { this.local[id] = {}; @@ -32,11 +31,13 @@ class ListenerMap { if (bubbles) { this.global[event_name].active--; 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]; } - } - else { + } else { const id = element.getAttribute("data-dioxus-id"); delete this.local[id][event_name]; if (this.local[id].length === 0) { @@ -143,8 +144,7 @@ class Interpreter { SetAttribute(id, field, value, ns) { if (value === null) { this.RemoveAttribute(id, field, ns); - } - else { + } else { const node = this.nodes[id]; this.SetAttributeInner(node, field, value, ns); } @@ -342,7 +342,6 @@ class Interpreter { this.RemoveEventListener(edit.id, edit.name); break; case "NewEventListener": - let bubbles = event_bubbles(edit.name); // this handler is only provided on desktop implementations since this @@ -360,7 +359,10 @@ class Interpreter { let a_element = target.closest("a"); if (a_element != null) { event.preventDefault(); - if (shouldPreventDefault !== `onclick` && a_element.getAttribute(`dioxus-prevent-default`) !== `onclick`) { + 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( @@ -404,23 +406,10 @@ class Interpreter { target.tagName === "FORM" && (event.type === "submit" || event.type === "input") ) { - for (let x = 0; x < target.elements.length; x++) { - let element = target.elements[x]; - let name = element.getAttribute("name"); - if (name != null) { - if (element.getAttribute("type") === "checkbox") { - // @ts-ignore - contents.values[name] = element.checked ? "true" : "false"; - } else if (element.getAttribute("type") === "radio") { - if (element.checked) { - contents.values[name] = element.value; - } - } else { - // @ts-ignore - contents.values[name] = - element.value ?? element.textContent; - } - } + const formData = new FormData(target); + + for (let name of formData.keys()) { + contents.values[name] = formData.getAll(name); } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 4d17749d8..5b56e663e 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -13,9 +13,10 @@ use dioxus_core::{ use dioxus_html::{event_bubbles, CompositionData, 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 wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast}; use web_sys::{Document, Element, Event, HtmlElement}; use crate::Config; @@ -325,35 +326,16 @@ fn read_input_to_data(target: Element) -> Rc { // try to fill in form values if let Some(form) = target.dyn_ref::() { - 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 = 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::().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::() { + if let Some(name) = array.get(0).as_string() { + if let Ok(item_values) = array.get(1).dyn_into::() { + let item_values = + item_values.iter().filter_map(|v| v.as_string()).collect(); + + values.insert(name, item_values); + } } } } @@ -366,6 +348,23 @@ fn read_input_to_data(target: Element) -> Rc { }) } +// 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() From bed1d58a77f420a1c0f7303f1c48fcfbe0c6e5ac Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 18 Apr 2023 09:24:34 -0500 Subject: [PATCH 2/9] create web file engine --- packages/web/Cargo.toml | 4 ++ packages/web/src/dom.rs | 20 +++++-- packages/web/src/file_engine.rs | 103 ++++++++++++++++++++++++++++++++ packages/web/src/lib.rs | 1 + 4 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 packages/web/src/file_engine.rs diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7adfbf13a..fcbb996bb 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -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] diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 4d17749d8..e792c977d 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -10,15 +10,15 @@ 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 rustc_hash::FxHashMap; -use std::{any::Any, rc::Rc}; +use std::{any::Any, rc::Rc, sync::Arc}; use wasm_bindgen::{closure::Closure, 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 { document: Document, @@ -206,6 +206,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 +225,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 { use dioxus_html::events::*; + console::log_1(&event.clone().into()); match event.type_().as_str() { "copy" | "cut" | "paste" => Rc::new(ClipboardData {}), @@ -359,10 +361,18 @@ fn read_input_to_data(target: Element) -> Rc { } } + 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) + }) + }); + Rc::new(FormData { value, values, - files: None, + files, }) } diff --git a/packages/web/src/file_engine.rs b/packages/web/src/file_engine.rs new file mode 100644 index 000000000..6552be084 --- /dev/null +++ b/packages/web/src/file_engine.rs @@ -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 { + 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 { + self.file_list.item(index as u32) + } + + fn find(&self, name: &str) -> Option { + (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 { + (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> { + let file = self.find(file)?; + + let file_reader = self.file_reader.clone(); + let (rx, tx) = oneshot::channel(); + let on_load: Closure = 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 { + let file = self.find(file)?; + + let file_reader = self.file_reader.clone(); + let (rx, tx) = oneshot::channel(); + let on_load: Closure = 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 + } + } +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index e208b0591..14705b85a 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -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; From d7eae795094f810d27833f3efd0d22696f347a42 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 20 Apr 2023 10:10:52 -0500 Subject: [PATCH 3/9] prevent default file input behavior on desktop --- packages/desktop/src/protocol.rs | 18 +- packages/interpreter/src/interpreter.js | 208 ++++++++++++------------ 2 files changed, 124 insertions(+), 102 deletions(-) diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index efdc849a5..3d4246987 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -9,10 +9,26 @@ 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) { + 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") { + window.ipc.postMessage(serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), multiple: target.hasAttribute("multiple") })); + event.preventDefault(); + } + }); + }"#, + ); format!( r#" From d749fff11b06718215f8d1bb77b06799c7a23d1f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 25 Apr 2023 14:29:35 -0500 Subject: [PATCH 8/9] fix double diologs on desktop renderer --- packages/desktop/src/lib.rs | 13 +++++++++---- packages/desktop/src/protocol.rs | 3 +-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 4d904e948..807ebf48e 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -273,16 +273,21 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) let event_name = &file_diolog.event; let event_bubbles = file_diolog.bubbles; let files = file_upload::get_file_event(&file_diolog); - let data = FormData { + 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(); - view.dom - .handle_event(event_name, Rc::new(data), id, event_bubbles); + 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); } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index f6725c428..e35d67bb1 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -27,8 +27,7 @@ fn module_loader(root_name: &str) -> String { 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"); - send("input"); + send("change&input"); } event.preventDefault(); } From 2f8d6fb49f9240b4a084d7ce381cba7a98998473 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 25 Apr 2023 15:07:22 -0500 Subject: [PATCH 9/9] fix clippy --- examples/file_upload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/file_upload.rs b/examples/file_upload.rs index c43e6e6ef..700c5392e 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -19,7 +19,7 @@ fn App(cx: Scope) -> Element { 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{ + if let Some(file) = file_engine.read_file_to_string(file_name).await{ files_uploaded.write().push(file); } }