From 403e8e2f4908d094eb25501dfc3218329aeb8482 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Mar 2024 13:00:27 -0800 Subject: [PATCH] Clean up file uploads in desktop/liveview, remove minify --- .vscode/settings.json | 3 +- Cargo.lock | 49 +------------- examples/file_upload.rs | 35 ++++++---- packages/desktop/js/prevent_file_upload.js | 23 ------- packages/desktop/src/app.rs | 31 ++------- packages/desktop/src/events.rs | 10 ++- packages/desktop/src/file_upload.rs | 20 +++++- packages/desktop/src/protocol.rs | 30 ++++----- packages/html/src/events.rs | 4 ++ packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 33 ++++++++-- packages/interpreter/src/ts/native.ts | 75 +++++++++++++++++++--- packages/liveview/Cargo.toml | 1 - packages/liveview/src/lib.rs | 9 +-- 14 files changed, 169 insertions(+), 156 deletions(-) delete mode 100644 packages/desktop/js/prevent_file_upload.js diff --git a/.vscode/settings.json b/.vscode/settings.json index e386d4b4c..1ebfd79ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,8 @@ "[toml]": { "editor.formatOnSave": false }, - "rust-analyzer.check.workspace": true, + "rust-analyzer.check.workspace": false, + // "rust-analyzer.check.workspace": true, "rust-analyzer.check.features": "all", "rust-analyzer.cargo.features": "all", "rust-analyzer.check.allTargets": true diff --git a/Cargo.lock b/Cargo.lock index ae75345b4..d928facd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,15 +83,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - [[package]] name = "aho-corasick" version = "1.1.2" @@ -2601,7 +2592,6 @@ dependencies = [ "futures-channel", "futures-util", "generational-box", - "minify-js", "pretty_env_logger", "rustc-hash", "serde", @@ -4084,7 +4074,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ - "aho-corasick 1.1.2", + "aho-corasick", "bstr 1.9.1", "log", "regex-automata", @@ -4437,16 +4427,6 @@ dependencies = [ "ahash 0.7.8", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash 0.8.10", - "bumpalo", -] - [[package]] name = "hashbrown" version = "0.14.3" @@ -5898,16 +5878,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minify-js" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22d6c512a82abddbbc13b70609cb2beff01be2c7afff534d6e5e1c85e438fc8b" -dependencies = [ - "lazy_static", - "parse-js", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -6619,19 +6589,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "parse-js" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ec3b11d443640ec35165ee8f6f0559f1c6f41878d70330fe9187012b5935f02" -dependencies = [ - "aho-corasick 0.7.20", - "bumpalo", - "hashbrown 0.13.2", - "lazy_static", - "memchr", -] - [[package]] name = "password-hash" version = "0.4.2" @@ -7562,7 +7519,7 @@ version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ - "aho-corasick 1.1.2", + "aho-corasick", "memchr", "regex-automata", "regex-syntax", @@ -7574,7 +7531,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ - "aho-corasick 1.1.2", + "aho-corasick", "memchr", "regex-syntax", ] diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 89669a248..4fe2a5a5b 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -3,9 +3,10 @@ //! Dioxus intercepts these events and provides a Rusty interface to the file data. Since we want this interface to //! be crossplatform, -use dioxus::html::HasFileData; +use std::sync::Arc; + use dioxus::prelude::*; -use tokio::time::sleep; +use dioxus::{html::HasFileData, prelude::dioxus_elements::FileEngine}; fn main() { launch(app); @@ -15,22 +16,24 @@ fn app() -> Element { let mut enable_directory_upload = use_signal(|| false); let mut files_uploaded = use_signal(|| Vec::new() as Vec); + let read_files = move |file_engine: Arc| async move { + 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); + } + } + }; + let upload_files = move |evt: FormEvent| async move { - for file_name in evt.files().unwrap().files() { - // no files on form inputs? - sleep(std::time::Duration::from_secs(1)).await; - files_uploaded.write().push(file_name); + if let Some(file_engine) = evt.files() { + read_files(file_engine).await; } }; let handle_file_drop = move |evt: DragEvent| 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); - } - } + if let Some(file_engine) = evt.files() { + read_files(file_engine).await; } }; @@ -50,20 +53,24 @@ fn app() -> Element { r#type: "file", accept: ".txt,.rs", multiple: true, + name: "textreader", directory: enable_directory_upload, onchange: upload_files, } + label { r#for: "textreader", "Upload text/rust files and read them" } + div { // cheating with a little bit of JS... "ondragover": "this.style.backgroundColor='#88FF88';", "ondragleave": "this.style.backgroundColor='#FFFFFF';", id: "drop-zone", - prevent_default: "ondrop dragover dragenter", + // prevent_default: "ondrop dragover dragenter", ondrop: handle_file_drop, ondragover: move |event| event.stop_propagation(), "Drop files here" } + ul { for file in files_uploaded.read().iter() { li { "{file}" } diff --git a/packages/desktop/js/prevent_file_upload.js b/packages/desktop/js/prevent_file_upload.js deleted file mode 100644 index 935415178..000000000 --- a/packages/desktop/js/prevent_file_upload.js +++ /dev/null @@ -1,23 +0,0 @@ -// 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")) { - // prevent file inputs from opening the file dialog on click - const type = input.getAttribute("type"); - if (type === "file") { - input.setAttribute("data-dioxus-file-listener", true); - input.addEventListener("click", (event) => { - let target = event.target; - let target_id = find_real_id(target); - if (target_id !== null) { - const send = (event_name) => { - const message = window.interpreter.serializeIpcMessage("file_dialog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", 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(); - }); - } - } - } diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 9adf6bc96..2927ff6c8 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -2,9 +2,8 @@ use crate::{ config::{Config, WindowCloseBehaviour}, element::DesktopElement, event_handlers::WindowEventHandlers, - file_upload::FileDialogRequest, - ipc::IpcMessage, - ipc::{EventData, UserWindowEvent}, + file_upload::{DesktopFileUploadForm, FileDialogRequest}, + ipc::{EventData, IpcMessage, UserWindowEvent}, query::QueryResult, shortcut::{GlobalHotKeyEvent, ShortcutRegistry}, webview::WebviewInstance, @@ -12,10 +11,7 @@ use crate::{ use crossbeam_channel::Receiver; use dioxus_core::ElementId; use dioxus_core::VirtualDom; -use dioxus_html::{ - native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData, HtmlEvent, - PlatformEventData, -}; +use dioxus_html::{native_bind::NativeFileEngine, HtmlEvent, PlatformEventData}; use std::{ cell::{Cell, RefCell}, collections::HashMap, @@ -270,30 +266,17 @@ impl App { let Ok(file_dialog) = serde_json::from_value::(msg.params()) else { return; }; - struct DesktopFileUploadForm { - files: Arc, - } - - impl HasFileData for DesktopFileUploadForm { - fn files(&self) -> Option> { - Some(self.files.clone()) - } - } - - impl HasFormData for DesktopFileUploadForm { - fn as_any(&self) -> &dyn std::any::Any { - self - } - } let id = ElementId(file_dialog.target); let event_name = &file_dialog.event; let event_bubbles = file_dialog.bubbles; let files = file_dialog.get_file_event(); - let data = Rc::new(PlatformEventData::new(Box::new(DesktopFileUploadForm { + let as_any = Box::new(DesktopFileUploadForm { files: Arc::new(NativeFileEngine::new(files)), - }))); + }); + + let data = Rc::new(PlatformEventData::new(as_any)); let view = self.webviews.get_mut(&window).unwrap(); diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index ac5a3df0d..e14c04a01 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -1,6 +1,6 @@ //! Convert a serialized event to an event trigger -use crate::element::DesktopElement; +use crate::{element::DesktopElement, file_upload::DesktopFileUploadForm}; use dioxus_html::*; pub(crate) struct SerializedHtmlEventConverter; @@ -47,8 +47,14 @@ impl HtmlEventConverter for SerializedHtmlEventConverter { } fn convert_form_data(&self, event: &PlatformEventData) -> FormData { + // Attempt a simple serialized form data conversion + if let Some(_data) = event.downcast::() { + return _data.clone().into(); + } + + // If that failed then it's a file upload form event - .downcast::() + .downcast::() .cloned() .unwrap() .into() diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index 98dbec437..c2bd651c5 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -1,7 +1,8 @@ #![allow(unused)] +use dioxus_html::{native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData}; use serde::Deserialize; -use std::{path::PathBuf, str::FromStr}; +use std::{path::PathBuf, str::FromStr, sync::Arc}; #[derive(Debug, Deserialize)] pub(crate) struct FileDialogRequest { @@ -124,3 +125,20 @@ impl FromStr for Filters { } } } + +#[derive(Clone)] +pub(crate) struct DesktopFileUploadForm { + pub files: Arc, +} + +impl HasFileData for DesktopFileUploadForm { + fn files(&self) -> Option> { + Some(self.files.clone()) + } +} + +impl HasFormData for DesktopFileUploadForm { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index ae3da5ba4..870c6cabb 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -13,18 +13,6 @@ const EDITS_PATH: &str = "http://dioxus.index.html/edits"; #[cfg(not(any(target_os = "android", target_os = "windows")))] const EDITS_PATH: &str = "dioxus://index.html/edits"; -// const PREVENT_FILE_UPLOAD: &str = include_str!("../js/prevent_file_upload.js"); - -fn handle_edits_code() -> String { - format!( - r#"// Poll for requests - {SLEDGEHAMMER_JS} - {NATIVE_JS} - window.interpreter = new NativeInterpreter("{EDITS_PATH}"); - "# - ) -} - static DEFAULT_INDEX: &str = include_str!("./index.html"); /// Build the index.html file we use for bootstrapping a new app @@ -184,20 +172,26 @@ fn serve_from_fs(path: PathBuf) -> Result>> { /// - headless: is this page being loaded but invisible? Important because not all windows are visible and the /// interpreter can't connect until the window is ready. fn module_loader(root_id: &str, headless: bool) -> String { - let js = handle_edits_code(); format!( r#" "# diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 9b58fc11c..0a761f719 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -59,6 +59,10 @@ impl PlatformEventData { Self { event } } + pub fn inner(&self) -> &Box { + &self.event + } + pub fn downcast(&self) -> Option<&T> { self.event.downcast_ref::() } diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 00ba413f1..31196d748 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -5236313184333634830 \ No newline at end of file +12023679989252671232 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 376eac052..b47df8f8a 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -214,7 +214,7 @@ var serializeDragEvent = function(event) { }; // src/ts/native.ts -var targetId = function(target) { +var getTargetId = function(target) { if (!(target instanceof Node)) { return null; } @@ -254,10 +254,30 @@ class NativeInterpreter extends JSChannel_ { } }, false); window.addEventListener("drop", function(e) { - if (e.target instanceof Element && e.target.tagName != "INPUT") { - e.preventDefault(); + let target = e.target; + if (!(target instanceof Element)) { + return; } + e.preventDefault(); }, false); + window.addEventListener("click", (event) => { + const target = event.target; + if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") { + let target_id = getTargetId(target); + if (target_id !== null) { + const message = this.serializeIpcMessage("file_dialog", { + event: "change&input", + accept: target.getAttribute("accept"), + directory: target.getAttribute("webkitdirectory") === "true", + multiple: target.hasAttribute("multiple"), + target: target_id, + bubbles: event.bubbles + }); + this.ipc.postMessage(message); + } + event.preventDefault(); + } + }); this.ipc = window.ipc; const handler = (event) => this.handleEvent(event, event.type, true); super.initialize(root, handler); @@ -310,9 +330,8 @@ class NativeInterpreter extends JSChannel_ { } } handleEvent(event, name, bubbles) { - console.log("handling event: ", event); const target = event.target; - const realId = targetId(target); + const realId = getTargetId(target); const contents = serializeEvent(event, target); let body = { name, @@ -386,14 +405,14 @@ class NativeInterpreter extends JSChannel_ { } } } - wait_for_request(headless) { + waitForRequest(headless) { fetch(new Request(this.editsPath)).then((response) => response.arrayBuffer()).then((bytes) => { if (headless) { this.run_from_bytes(bytes); } else { requestAnimationFrame(() => this.run_from_bytes(bytes)); } - this.wait_for_request(headless); + this.waitForRequest(headless); }); } } diff --git a/packages/interpreter/src/ts/native.ts b/packages/interpreter/src/ts/native.ts index 13bb152e2..9755421a8 100644 --- a/packages/interpreter/src/ts/native.ts +++ b/packages/interpreter/src/ts/native.ts @@ -36,19 +36,49 @@ export class NativeInterpreter extends JSChannel_ { // attach an event listener on the body that prevents file drops from navigating // this is because the browser will try to navigate to the file if it's dropped on the window window.addEventListener("dragover", function (e) { - // check which element is our target + // // check which element is our target if (e.target instanceof Element && e.target.tagName != "INPUT") { e.preventDefault(); } }, false); window.addEventListener("drop", function (e) { - // check which element is our target - if (e.target instanceof Element && e.target.tagName != "INPUT") { - e.preventDefault(); + let target = e.target; + + if (!(target instanceof Element)) { + return; } + + // Dropping a file on the window will navigate to the file, which we don't want + e.preventDefault(); + + // if the element has a drop listener on it, we should send a message to the host with the contents of the drop instead + }, false); + // attach a listener to the route that listens for clicks and prevents the default file dialog + window.addEventListener("click", (event) => { + const target = event.target; + if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") { + // Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it + let target_id = getTargetId(target); + if (target_id !== null) { + const message = this.serializeIpcMessage("file_dialog", { + event: "change&input", + accept: target.getAttribute("accept"), + directory: target.getAttribute("webkitdirectory") === "true", + multiple: target.hasAttribute("multiple"), + target: target_id, + bubbles: event.bubbles, + }); + this.ipc.postMessage(message); + } + + // Prevent default regardless - we don't want file dialogs and we don't want the browser to navigate + event.preventDefault(); + } + }); + // @ts-ignore - wry gives us this this.ipc = window.ipc; @@ -119,10 +149,8 @@ export class NativeInterpreter extends JSChannel_ { } handleEvent(event: Event, name: string, bubbles: boolean) { - console.log("handling event: ", event); - const target = event.target!; - const realId = targetId(target)!; + const realId = getTargetId(target)!; const contents = serializeEvent(event, target); // Handle the event on the virtualdom and then preventDefault if it also preventsDefault @@ -141,6 +169,8 @@ export class NativeInterpreter extends JSChannel_ { this.preventDefaults(event, target); + + // liveview does not have syncronous event handling, so we need to send the event to the host if (this.liveview) { // Okay, so the user might've requested some files to be read @@ -263,7 +293,7 @@ export class NativeInterpreter extends JSChannel_ { } } - wait_for_request(headless: boolean) { + waitForRequest(headless: boolean) { fetch(new Request(this.editsPath)) .then(response => response.arrayBuffer()) .then(bytes => { @@ -275,7 +305,7 @@ export class NativeInterpreter extends JSChannel_ { // @ts-ignore requestAnimationFrame(() => this.run_from_bytes(bytes)); } - this.wait_for_request(headless); + this.waitForRequest(headless); }); } } @@ -306,7 +336,7 @@ function handleVirtualdomEventSync(contents: string): EventSyncResult { return JSON.parse(xhr.responseText); } -function targetId(target: EventTarget): NodeId | null { +function getTargetId(target: EventTarget): NodeId | null { // Ensure that the target is a node, sometimes it's nota if (!(target instanceof Node)) { return null; @@ -329,3 +359,28 @@ function targetId(target: EventTarget): NodeId | null { return parseInt(realId); } + + +// function applyFileUpload() { +// let inputs = document.querySelectorAll("input"); +// for (let input of inputs) { +// if (!input.getAttribute("data-dioxus-file-listener")) { +// // prevent file inputs from opening the file dialog on click +// const type = input.getAttribute("type"); +// if (type === "file") { +// input.setAttribute("data-dioxus-file-listener", true); +// input.addEventListener("click", (event) => { +// let target = event.target; +// let target_id = find_real_id(target); +// if (target_id !== null) { +// const send = (event_name) => { +// const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", 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(); +// }); +// } +// } +// } diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 6e7173e18..dac3ea6b6 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -24,7 +24,6 @@ serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" dioxus-html = { workspace = true, features = ["serialize", "eval", "mounted"] } rustc-hash = { workspace = true } -minify-js = "0.5.6" dioxus-core = { workspace = true, features = ["serialize"] } dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 121f0245c..33f916785 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -32,7 +32,6 @@ pub enum LiveViewError { fn handle_edits_code() -> String { use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; - use minify_js::{minify, Session, TopLevelMode}; let serialize_file_uploads = r#"if ( target.tagName === "INPUT" && @@ -81,15 +80,9 @@ fn handle_edits_code() -> String { .unwrap_or_else(|| interpreter.len()); interpreter.replace_range(import_start..import_end, ""); } - let main_js = include_str!("./main.js"); - let js = format!("{interpreter}\n{main_js}"); - - let session = Session::new(); - let mut out = Vec::new(); - minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap(); - String::from_utf8(out).unwrap() + js } /// This script that gets injected into your app connects this page to the websocket endpoint