瀏覽代碼

clean up native js more

Jonathan Kelley 1 年之前
父節點
當前提交
56a365742c

+ 87 - 78
examples/form.rs

@@ -12,95 +12,104 @@ fn main() {
 
 fn app() -> Element {
     let mut values = use_signal(|| HashMap::new());
+    let mut submitted_values = use_signal(|| HashMap::new());
+
     rsx! {
-        div {
-            h1 { "Form" }
-
-            // The form element is used to create an HTML form for user input
-            // You can attach regular attributes to it
-            form {
-                id: "cool-form",
-                style: "display: flex; flex-direction: column;",
-
-                // You can attach a handler to the entire form
-                oninput: move |ev| {
-                    println!("Input event: {:#?}", ev);
-                    values.set(ev.values());
-                },
-
-                // On desktop/liveview, the form will not navigate the page - the expectation is that you handle
-                // The form event.
-                // Howver, if your form doesn't have a submit handler, it might navigate the page depending on the webview.
-                // We suggest always attaching a submit handler to the form.
-                onsubmit: move |ev| {
-                    println!("Submit event: {:#?}", ev);
-                },
-
-                // Regular text inputs with handlers
-                label { r#for: "username", "Username" }
-                input {
-                    r#type: "text",
-                    name: "username",
-                    oninput: move |ev| {
-                        println!("setting username");
-                        values.set(ev.values());
-                    }
+        div { style: "display: flex",
+            div { style: "width: 50%",
+                h1 { "Form" }
+
+                if !submitted_values.read().is_empty() {
+                    h2 { "Submitted! ✅" }
                 }
 
-                // And then the various inputs that might exist
-                // Note for a value to be returned in .values(), it must be named!
+                // The form element is used to create an HTML form for user input
+                // You can attach regular attributes to it
+                form {
+                    id: "cool-form",
+                    style: "display: flex; flex-direction: column;",
 
-                label { r#for: "full-name", "Full Name" }
-                input { r#type: "text", name: "full-name" }
+                    // You can attach a handler to the entire form
+                    oninput: move |ev| {
+                        println!("Input event: {:#?}", ev);
+                        values.set(ev.values());
+                    },
 
-                label { r#for: "email", "Email" }
-                input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" }
+                    // On desktop/liveview, the form will not navigate the page - the expectation is that you handle
+                    // The form event.
+                    // Howver, if your form doesn't have a submit handler, it might navigate the page depending on the webview.
+                    // We suggest always attaching a submit handler to the form.
+                    onsubmit: move |ev| {
+                        println!("Submit event: {:#?}", ev);
+                        submitted_values.set(ev.values());
+                    },
 
-                label { r#for: "password", "Password" }
-                input { r#type: "password", name: "password" }
+                    // Regular text inputs with handlers
+                    label { r#for: "username", "Username" }
+                    input {
+                        r#type: "text",
+                        name: "username",
+                        oninput: move |ev| {
+                            println!("setting username");
+                            values.set(ev.values());
+                        }
+                    }
 
-                label { r#for: "color", "Color" }
-                input { r#type: "radio", checked: true, name: "color", value: "red" }
-                input { r#type: "radio", name: "color", value: "blue" }
-                input { r#type: "radio", name: "color", value: "green" }
+                    // And then the various inputs that might exist
+                    // Note for a value to be returned in .values(), it must be named!
+
+                    label { r#for: "full-name", "Full Name" }
+                    input { r#type: "text", name: "full-name" }
+
+                    label { r#for: "email", "Email" }
+                    input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" }
+
+                    label { r#for: "password", "Password" }
+                    input { r#type: "password", name: "password" }
+
+                    label { r#for: "color", "Color" }
+                    input { r#type: "radio", checked: true, name: "color", value: "red" }
+                    input { r#type: "radio", name: "color", value: "blue" }
+                    input { r#type: "radio", name: "color", value: "green" }
+
+                    // Select multiple comes in as a comma separated list of selected values
+                    // You should split them on the comma to get the values manually
+                    label { r#for: "country", "Country" }
+                    select {
+                        name: "country",
+                        multiple: true,
+                        oninput: move |ev| {
+                            println!("Input event: {:#?}", ev);
+                            println!("Values: {:#?}", ev.value().split(',').collect::<Vec<_>>());
+                        },
+                        option { value: "usa",  "USA" }
+                        option { value: "canada",  "Canada" }
+                        option { value: "mexico",  "Mexico" }
+                    }
 
-                // Select multiple comes in as a comma separated list of selected values
-                // You should split them on the comma to get the values manually
-                label { r#for: "country", "Country" }
-                select {
-                    name: "country",
-                    multiple: true,
-                    oninput: move |ev| {
-                        println!("Input event: {:#?}", ev);
-                        println!("Values: {:#?}", ev.value().split(',').collect::<Vec<_>>());
-                    },
-                    option { value: "usa",  "USA" }
-                    option { value: "canada",  "Canada" }
-                    option { value: "mexico",  "Mexico" }
-                }
+                    // Safari can be quirky with color inputs on mac.
+                    // We recommend always providing a text input for color as a fallback.
+                    label { r#for: "color", "Color" }
+                    input { r#type: "color", value: "#000002", name: "head", id: "head" }
+
+                    // Dates!
+                    input {
+                        min: "2018-01-01",
+                        value: "2018-07-22",
+                        r#type: "date",
+                        name: "trip-start",
+                        max: "2025-12-31",
+                        id: "start"
+                    }
 
-                // Safari can be quirky with color inputs on mac.
-                // We recommend always providing a text input for color as a fallback.
-                label { r#for: "color", "Color" }
-                input { r#type: "color", value: "#000002", name: "head", id: "head" }
-
-                // Dates!
-                input {
-                    min: "2018-01-01",
-                    value: "2018-07-22",
-                    r#type: "date",
-                    name: "trip-start",
-                    max: "2025-12-31",
-                    id: "start"
+                    // Buttons will submit your form by default.
+                    button { r#type: "submit", value: "Submit", "Submit the form" }
                 }
-
-                // Buttons will submit your form by default.
-                button { r#type: "submit", value: "Submit", "Submit the form" }
             }
-        }
-        div {
-            h1 { "Oninput Values" }
-            pre { "{values:#?}" }
+            div { style: "width: 50%",
+                h1 { "Oninput Values" }
+                pre { "{values:#?}" }
+            }
         }
     }
 }

+ 9 - 38
packages/desktop/src/protocol.rs

@@ -1,5 +1,6 @@
 use crate::{assets::*, edits::EditQueue};
-use dioxus_interpreter_js::unified_bindings::native_js;
+use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
+use dioxus_interpreter_js::NATIVE_JS;
 use std::path::{Path, PathBuf};
 use wry::{
     http::{status::StatusCode, Request, Response},
@@ -12,46 +13,16 @@ 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");
+// const PREVENT_FILE_UPLOAD: &str = include_str!("../js/prevent_file_upload.js");
 
 fn handle_edits_code() -> String {
-    let polling_request = format!(
+    format!(
         r#"// Poll for requests
-    window.interpreter = new JSChannel();
-    window.interpreter.wait_for_request = (headless) => {{
-      fetch(new Request("{EDITS_PATH}"))
-          .then(response => {{
-              response.arrayBuffer()
-                  .then(bytes => {{
-                      // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly
-                      if (headless) {{
-                        window.interpreter.run_from_bytes(bytes);
-                      }}
-                      else {{
-                        requestAnimationFrame(() => {{
-                            window.interpreter.run_from_bytes(bytes);
-                        }});
-                      }}
-                      window.interpreter.wait_for_request(headless);
-                  }});
-          }})
-    }}"#
-    );
-
-    let mut interpreter = native_js()
-        .replace("/*POST_HANDLE_EDITS*/", PREVENT_FILE_UPLOAD)
-        .replace("export", "")
-        + &polling_request;
-
-    while let Some(import_start) = interpreter.find("import") {
-        let import_end = interpreter[import_start..]
-            .find(|c| c == ';' || c == '\n')
-            .map(|i| i + import_start)
-            .unwrap_or_else(|| interpreter.len());
-        interpreter.replace_range(import_start..import_end, "");
-    }
-
-    format!("{interpreter}\nconst intercept_link_redirects = true;")
+        {SLEDGEHAMMER_JS}
+        {NATIVE_JS}
+        window.interpreter = new NativeInterpreter("{EDITS_PATH}");
+    "#
+    )
 }
 
 static DEFAULT_INDEX: &str = include_str!("./index.html");

+ 1 - 3
packages/interpreter/src/js/core.js

@@ -88,9 +88,7 @@ class BaseInterpreter {
   stack;
   templates;
   m;
-  constructor(root, handler) {
-    this.handler = handler;
-    this.initialize(root);
+  constructor() {
   }
   initialize(root, handler = null) {
     this.global = {};

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

@@ -1 +1 @@
-7906913262979346544
+5236313184333634830

+ 28 - 216
packages/interpreter/src/js/native.js

@@ -1,210 +1,3 @@
-// src/ts/set_attribute.ts
-function setAttributeInner(node, field, value, ns) {
-  if (ns === "style") {
-    node.style.setProperty(field, value);
-    return;
-  }
-  if (!!ns) {
-    node.setAttributeNS(ns, field, value);
-    return;
-  }
-  switch (field) {
-    case "value":
-      if (node.value !== value) {
-        node.value = value;
-      }
-      break;
-    case "initial_value":
-      node.defaultValue = value;
-      break;
-    case "checked":
-      node.checked = truthy(value);
-      break;
-    case "initial_checked":
-      node.defaultChecked = truthy(value);
-      break;
-    case "selected":
-      node.selected = truthy(value);
-      break;
-    case "initial_selected":
-      node.defaultSelected = truthy(value);
-      break;
-    case "dangerous_inner_html":
-      node.innerHTML = value;
-      break;
-    default:
-      if (!truthy(value) && isBoolAttr(field)) {
-        node.removeAttribute(field);
-      } else {
-        node.setAttribute(field, value);
-      }
-  }
-}
-var truthy = function(val) {
-  return val === "true" || val === true;
-};
-var isBoolAttr = function(field) {
-  switch (field) {
-    case "allowfullscreen":
-    case "allowpaymentrequest":
-    case "async":
-    case "autofocus":
-    case "autoplay":
-    case "checked":
-    case "controls":
-    case "default":
-    case "defer":
-    case "disabled":
-    case "formnovalidate":
-    case "hidden":
-    case "ismap":
-    case "itemscope":
-    case "loop":
-    case "multiple":
-    case "muted":
-    case "nomodule":
-    case "novalidate":
-    case "open":
-    case "playsinline":
-    case "readonly":
-    case "required":
-    case "reversed":
-    case "selected":
-    case "truespeed":
-    case "webkitdirectory":
-      return true;
-    default:
-      return false;
-  }
-};
-
-// src/ts/core.ts
-class BaseInterpreter {
-  global;
-  local;
-  root;
-  handler;
-  nodes;
-  stack;
-  templates;
-  m;
-  constructor(root, handler) {
-    this.handler = handler;
-    this.initialize(root);
-  }
-  initialize(root, handler = null) {
-    this.global = {};
-    this.local = {};
-    this.root = root;
-    this.nodes = [root];
-    this.stack = [root];
-    this.templates = {};
-    if (handler) {
-      this.handler = handler;
-    }
-  }
-  createListener(event_name, element, bubbles) {
-    if (bubbles) {
-      if (this.global[event_name] === undefined) {
-        this.global[event_name] = { active: 1, callback: this.handler };
-        this.root.addEventListener(event_name, this.handler);
-      } else {
-        this.global[event_name].active++;
-      }
-    } else {
-      const id = element.getAttribute("data-dioxus-id");
-      if (!this.local[id]) {
-        this.local[id] = {};
-      }
-      element.addEventListener(event_name, this.handler);
-    }
-  }
-  removeListener(element, event_name, bubbles) {
-    if (bubbles) {
-      this.removeBubblingListener(event_name);
-    } else {
-      this.removeNonBubblingListener(element, event_name);
-    }
-  }
-  removeBubblingListener(event_name) {
-    this.global[event_name].active--;
-    if (this.global[event_name].active === 0) {
-      this.root.removeEventListener(event_name, this.global[event_name].callback);
-      delete this.global[event_name];
-    }
-  }
-  removeNonBubblingListener(element, event_name) {
-    const id = element.getAttribute("data-dioxus-id");
-    delete this.local[id][event_name];
-    if (Object.keys(this.local[id]).length === 0) {
-      delete this.local[id];
-    }
-    element.removeEventListener(event_name, this.handler);
-  }
-  removeAllNonBubblingListeners(element) {
-    const id = element.getAttribute("data-dioxus-id");
-    delete this.local[id];
-  }
-  getNode(id) {
-    return this.nodes[id];
-  }
-  appendChildren(id, many) {
-    const root = this.nodes[id];
-    const els = this.stack.splice(this.stack.length - many);
-    for (let k = 0;k < many; k++) {
-      root.appendChild(els[k]);
-    }
-  }
-  loadChild(ptr, len) {
-    let node = this.stack[this.stack.length - 1];
-    let ptr_end = ptr + len;
-    for (;ptr < ptr_end; ptr++) {
-      let end = this.m.getUint8(ptr);
-      for (node = node.firstChild;end > 0; end--) {
-        node = node.nextSibling;
-      }
-    }
-    return node;
-  }
-  saveTemplate(nodes, tmpl_id) {
-    this.templates[tmpl_id] = nodes;
-  }
-  hydrateRoot(ids) {
-    const hydrateNodes = document.querySelectorAll("[data-node-hydration]");
-    for (let i = 0;i < hydrateNodes.length; i++) {
-      const hydrateNode = hydrateNodes[i];
-      const hydration = hydrateNode.getAttribute("data-node-hydration");
-      const split = hydration.split(",");
-      const id = ids[parseInt(split[0])];
-      this.nodes[id] = hydrateNode;
-      if (split.length > 1) {
-        hydrateNode.listening = split.length - 1;
-        hydrateNode.setAttribute("data-dioxus-id", id.toString());
-        for (let j = 1;j < split.length; j++) {
-          const listener = split[j];
-          const split2 = listener.split(":");
-          const event_name = split2[0];
-          const bubbles = split2[1] === "1";
-          this.createListener(event_name, hydrateNode, bubbles);
-        }
-      }
-    }
-    const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
-    let currentNode = treeWalker.nextNode();
-    while (currentNode) {
-      const id = currentNode.textContent;
-      const split = id.split("node-id");
-      if (split.length > 1) {
-        this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling;
-      }
-      currentNode = treeWalker.nextNode();
-    }
-  }
-  setAttributeInner(node, field, value, ns) {
-    setAttributeInner(node, field, value, ns);
-  }
-}
-
 // src/ts/form.ts
 function retriveValues(event, target) {
   let contents = {
@@ -438,13 +231,21 @@ var targetId = function(target) {
   }
   return parseInt(realId);
 };
+var JSChannel_;
+if (RawInterpreter) {
+  JSChannel_ = RawInterpreter;
+}
 
-class PlatformInterpreter extends BaseInterpreter {
+class NativeInterpreter extends JSChannel_ {
   intercept_link_redirects;
   ipc;
+  editsPath;
   liveview;
-  constructor(root) {
-    super(root, (event) => this.handleEvent(event, event.type, true));
+  constructor(editsPath) {
+    super();
+    this.editsPath = editsPath;
+  }
+  initialize(root) {
     this.intercept_link_redirects = true;
     this.liveview = false;
     window.addEventListener("dragover", function(e) {
@@ -458,13 +259,12 @@ class PlatformInterpreter extends BaseInterpreter {
       }
     }, false);
     this.ipc = window.ipc;
+    const handler = (event) => this.handleEvent(event, event.type, true);
+    super.initialize(root, handler);
   }
   serializeIpcMessage(method, params = {}) {
     return JSON.stringify({ method, params });
   }
-  setAttributeInner(node, field, value, ns) {
-    setAttributeInner(node, field, value, ns);
-  }
   scrollTo(id, behavior) {
     const node = this.nodes[id];
     if (node instanceof HTMLElement) {
@@ -492,7 +292,7 @@ class PlatformInterpreter extends BaseInterpreter {
       }
     }
   }
-  LoadChild(array) {
+  loadChild(array) {
     let node = this.stack[this.stack.length - 1];
     for (let i = 0;i < array.length; i++) {
       let end = array[i];
@@ -502,7 +302,7 @@ class PlatformInterpreter extends BaseInterpreter {
     }
     return node;
   }
-  AppendChildren(id, many) {
+  appendChildren(id, many) {
     const root = this.nodes[id];
     const els = this.stack.splice(this.stack.length - many);
     for (let k = 0;k < many; k++) {
@@ -510,6 +310,7 @@ class PlatformInterpreter extends BaseInterpreter {
     }
   }
   handleEvent(event, name, bubbles) {
+    console.log("handling event: ", event);
     const target = event.target;
     const realId = targetId(target);
     const contents = serializeEvent(event, target);
@@ -529,6 +330,7 @@ class PlatformInterpreter extends BaseInterpreter {
     } else {
       const message = this.serializeIpcMessage("user_event", body);
       this.ipc.postMessage(message);
+      console.log("sent message to host: ", message);
     }
   }
   async readFiles(target, contents, bubbles, realId, name) {
@@ -584,7 +386,17 @@ class PlatformInterpreter extends BaseInterpreter {
       }
     }
   }
+  wait_for_request(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);
+    });
+  }
 }
 export {
-  PlatformInterpreter
+  NativeInterpreter
 };

+ 1 - 4
packages/interpreter/src/ts/core.ts

@@ -28,10 +28,7 @@ export class BaseInterpreter {
   // sledgehammer is generating this...
   m: any;
 
-  constructor(root: HTMLElement, handler: EventListener) {
-    this.handler = handler;
-    this.initialize(root);
-  }
+  constructor() { }
 
   initialize(root: HTMLElement, handler: EventListener | null = null) {
     this.global = {};

+ 45 - 10
packages/interpreter/src/ts/native.ts

@@ -5,18 +5,31 @@
 
 import { BaseInterpreter, NodeId } from "./core";
 import { SerializedEvent, serializeEvent } from "./serialize";
-import { setAttributeInner } from "./set_attribute";
 
-export class PlatformInterpreter extends BaseInterpreter {
+// okay so, we've got this JSChannel thing from sledgehammer, implicitly imported into our scope
+// we want to extend it, and it technically extends base intepreter. To make typescript happy,
+// we're going to bind the JSChannel_ object to the JSChannel object, and then extend it
+var JSChannel_: typeof BaseInterpreter;
+
+// @ts-ignore - this is coming from the host
+if (RawInterpreter) { JSChannel_ = RawInterpreter; }
+
+export class NativeInterpreter extends JSChannel_ {
   intercept_link_redirects: boolean;
   ipc: any;
+  editsPath: string;
 
   // eventually we want to remove liveview and build it into the server-side-events of fullstack
   // however, for now we need to support it since SSE in fullstack doesn't exist yet
   liveview: boolean;
 
-  constructor(root: HTMLElement) {
-    super(root, (event) => this.handleEvent(event, event.type, true));
+  constructor(editsPath: string) {
+    super();
+    this.editsPath = editsPath;
+  }
+
+  initialize(root: HTMLElement): void {
+
     this.intercept_link_redirects = true;
     this.liveview = false;
 
@@ -39,16 +52,16 @@ export class PlatformInterpreter extends BaseInterpreter {
 
     // @ts-ignore - wry gives us this
     this.ipc = window.ipc;
+
+    // make sure we pass the handler to the base interpreter
+    const handler: EventListener = (event) => this.handleEvent(event, event.type, true);
+    super.initialize(root, handler);
   }
 
   serializeIpcMessage(method: string, params = {}) {
     return JSON.stringify({ method, params });
   }
 
-  setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) {
-    setAttributeInner(node, field, value, ns);
-  }
-
   scrollTo(id: NodeId, behavior: ScrollBehavior) {
     const node = this.nodes[id];
     if (node instanceof HTMLElement) {
@@ -80,7 +93,9 @@ export class PlatformInterpreter extends BaseInterpreter {
     }
   }
 
-  LoadChild(array: number[]) {
+  // ignore the fact the base interpreter uses ptr + len but we use array
+  // @ts-ignore
+  loadChild(array: number[]) {
     // iterate through each number and get that child
     let node = this.stack[this.stack.length - 1];
 
@@ -94,7 +109,7 @@ export class PlatformInterpreter extends BaseInterpreter {
     return node;
   }
 
-  AppendChildren(id: NodeId, many: number) {
+  appendChildren(id: NodeId, many: number) {
     const root = this.nodes[id];
     const els = this.stack.splice(this.stack.length - many);
 
@@ -104,6 +119,8 @@ export class PlatformInterpreter extends BaseInterpreter {
   }
 
   handleEvent(event: Event, name: string, bubbles: boolean) {
+    console.log("handling event: ", event);
+
     const target = event.target!;
     const realId = targetId(target)!;
     const contents = serializeEvent(event, target);
@@ -137,6 +154,8 @@ export class PlatformInterpreter extends BaseInterpreter {
       const message = this.serializeIpcMessage("user_event", body);
       this.ipc.postMessage(message);
 
+      console.log("sent message to host: ", message);
+
       // // Run the event handler on the virtualdom
       // // capture/prevent default of the event if the virtualdom wants to
       // const res = handleVirtualdomEventSync(JSON.stringify(body));
@@ -243,6 +262,22 @@ export class PlatformInterpreter extends BaseInterpreter {
       }
     }
   }
+
+  wait_for_request(headless: boolean) {
+    fetch(new Request(this.editsPath))
+      .then(response => response.arrayBuffer())
+      .then(bytes => {
+        // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly
+        if (headless) {
+          // @ts-ignore
+          this.run_from_bytes(bytes);
+        } else {
+          // @ts-ignore
+          requestAnimationFrame(() => this.run_from_bytes(bytes));
+        }
+        this.wait_for_request(headless);
+      });
+  }
 }
 
 type EventSyncResult = {