Browse Source

Clean up file uploads in desktop/liveview, remove minify

Jonathan Kelley 1 year ago
parent
commit
403e8e2f49

+ 2 - 1
.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

+ 3 - 46
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",
 ]

+ 21 - 14
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<String>);
 
+    let read_files = move |file_engine: Arc<dyn FileEngine>| 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}" }

+ 0 - 23
packages/desktop/js/prevent_file_upload.js

@@ -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();
-        });
-      }
-    }
-  }

+ 7 - 24
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::<FileDialogRequest>(msg.params()) else {
             return;
         };
-        struct DesktopFileUploadForm {
-            files: Arc<NativeFileEngine>,
-        }
-
-        impl HasFileData for DesktopFileUploadForm {
-            fn files(&self) -> Option<Arc<dyn FileEngine>> {
-                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();
 

+ 8 - 2
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::<SerializedFormData>() {
+            return _data.clone().into();
+        }
+
+        // If that failed then it's a file upload form
         event
-            .downcast::<SerializedFormData>()
+            .downcast::<DesktopFileUploadForm>()
             .cloned()
             .unwrap()
             .into()

+ 19 - 1
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<NativeFileEngine>,
+}
+
+impl HasFileData for DesktopFileUploadForm {
+    fn files(&self) -> Option<Arc<dyn FileEngine>> {
+        Some(self.files.clone())
+    }
+}
+
+impl HasFormData for DesktopFileUploadForm {
+    fn as_any(&self) -> &dyn std::any::Any {
+        self
+    }
+}

+ 12 - 18
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<Response<Vec<u8>>> {
 /// - 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#"
 <script type="module">
-    {js}
-    // Wait for the page to load
+    // Bring the sledgehammer code
+    {SLEDGEHAMMER_JS}
+
+    // And then extend it with our native bindings
+    {NATIVE_JS}
+
+    // The nativeinterprerter extends the sledgehammer interpreter with a few extra methods that we use for IPC
+    window.interpreter = new NativeInterpreter("{EDITS_PATH}");
+
+    // Wait for the page to load before sending the initialize message
     window.onload = function() {{
-        let rootname = "{root_id}";
-        let root_element = window.document.getElementById(rootname);
+        let root_element = window.document.getElementById("{root_id}");
         if (root_element != null) {{
             window.interpreter.initialize(root_element);
             window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
         }}
-        window.interpreter.wait_for_request({headless});
+        window.interpreter.waitForRequest({headless});
     }}
 </script>
 "#

+ 4 - 0
packages/html/src/events.rs

@@ -59,6 +59,10 @@ impl PlatformEventData {
         Self { event }
     }
 
+    pub fn inner(&self) -> &Box<dyn Any> {
+        &self.event
+    }
+
     pub fn downcast<T: 'static>(&self) -> Option<&T> {
         self.event.downcast_ref::<T>()
     }

+ 1 - 1
packages/interpreter/src/js/hash.txt

@@ -1 +1 @@
-5236313184333634830
+12023679989252671232

+ 26 - 7
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);
     });
   }
 }

+ 65 - 10
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();
+//         });
+//       }
+//     }
+// }

+ 0 - 1
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 }

+ 1 - 8
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