Forráskód Böngészése

Merge branch 'main' into jk/fix-form-inputs

Jonathan Kelley 1 éve
szülő
commit
206a48257b

+ 12 - 2
Cargo.lock

@@ -8524,9 +8524,19 @@ dependencies = [
 
 [[package]]
 name = "sledgehammer_bindgen"
-version = "0.3.1"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa1ca40134578bf7cf17973defcd4eb8d7d2adf7868b29892481722957bd543e"
+dependencies = [
+ "sledgehammer_bindgen_macro",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "sledgehammer_bindgen_macro"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9298e863f0143b89972299110a7fa3e2fc08c412341d588c497bae38409f9e68"
+checksum = "04559ded3de5c62f08457cadcb6c44649c4d90e72fdc0804c6c30ce1bc526304"
 dependencies = [
  "quote",
  "syn 2.0.50",

+ 4 - 3
packages/desktop/src/protocol.rs

@@ -17,6 +17,7 @@ const PREVENT_FILE_UPLOAD: &str = include_str!("../js/prevent_file_upload.js");
 fn handle_edits_code() -> String {
     let polling_request = format!(
         r#"// Poll for requests
+    window.interpreter = new JSChannel();
     window.interpreter.wait_for_request = (headless) => {{
       fetch(new Request("{EDITS_PATH}"))
           .then(response => {{
@@ -24,11 +25,11 @@ fn handle_edits_code() -> String {
                   .then(bytes => {{
                       // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly
                       if (headless) {{
-                        run_from_bytes(bytes);
+                        window.interpreter.run_from_bytes(bytes);
                       }}
                       else {{
                         requestAnimationFrame(() => {{
-                          run_from_bytes(bytes);
+                            window.interpreter.run_from_bytes(bytes);
                         }});
                       }}
                       window.interpreter.wait_for_request(headless);
@@ -50,7 +51,7 @@ fn handle_edits_code() -> String {
         interpreter.replace_range(import_start..import_end, "");
     }
 
-    format!("{interpreter}\nconst config = new InterpreterConfig(true);")
+    format!("{interpreter}\nconst intercept_link_redirects = true;")
 }
 
 static DEFAULT_INDEX: &str = include_str!("./index.html");

+ 1 - 1
packages/interpreter/Cargo.toml

@@ -17,7 +17,7 @@ web-sys = { version = "0.3.56", optional = true, features = [
     "Element",
     "Node",
 ] }
-sledgehammer_bindgen = { version = "0.3.1", default-features = false, optional = true }
+sledgehammer_bindgen = { version = "0.4.0", default-features = false, optional = true }
 sledgehammer_utils = { version = "0.2", optional = true }
 serde = { version = "1.0", features = ["derive"], optional = true }
 

+ 79 - 0
packages/interpreter/src/common.js

@@ -0,0 +1,79 @@
+this.setAttributeInner = function (node, field, value, ns) {
+  const name = field;
+  if (ns === "style") {
+    // ????? why do we need to do this
+    if (node.style === undefined) {
+      node.style = {};
+    }
+    node.style[name] = value;
+  } else if (!!ns) {
+    node.setAttributeNS(ns, name, value);
+  } else {
+    switch (name) {
+      case "value":
+        if (value !== node.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:
+        // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
+        if (!truthy(value) && bool_attrs.hasOwnProperty(name)) {
+          node.removeAttribute(name);
+        } else {
+          node.setAttribute(name, value);
+        }
+    }
+  }
+}
+
+const bool_attrs = {
+  allowfullscreen: true,
+  allowpaymentrequest: true,
+  async: true,
+  autofocus: true,
+  autoplay: true,
+  checked: true,
+  controls: true,
+  default: true,
+  defer: true,
+  disabled: true,
+  formnovalidate: true,
+  hidden: true,
+  ismap: true,
+  itemscope: true,
+  loop: true,
+  multiple: true,
+  muted: true,
+  nomodule: true,
+  novalidate: true,
+  open: true,
+  playsinline: true,
+  readonly: true,
+  required: true,
+  reversed: true,
+  selected: true,
+  truespeed: true,
+  webkitdirectory: true,
+};
+
+function truthy(val) {
+  return val === "true" || val === true;
+}

+ 79 - 0
packages/interpreter/src/common_exported.js

@@ -0,0 +1,79 @@
+export function setAttributeInner(node, field, value, ns) {
+  const name = field;
+  if (ns === "style") {
+    // ????? why do we need to do this
+    if (node.style === undefined) {
+      node.style = {};
+    }
+    node.style[name] = value;
+  } else if (!!ns) {
+    node.setAttributeNS(ns, name, value);
+  } else {
+    switch (name) {
+      case "value":
+        if (value !== node.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:
+        // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
+        if (!truthy(value) && bool_attrs.hasOwnProperty(name)) {
+          node.removeAttribute(name);
+        } else {
+          node.setAttribute(name, value);
+        }
+    }
+  }
+}
+
+const bool_attrs = {
+  allowfullscreen: true,
+  allowpaymentrequest: true,
+  async: true,
+  autofocus: true,
+  autoplay: true,
+  checked: true,
+  controls: true,
+  default: true,
+  defer: true,
+  disabled: true,
+  formnovalidate: true,
+  hidden: true,
+  ismap: true,
+  itemscope: true,
+  loop: true,
+  multiple: true,
+  muted: true,
+  nomodule: true,
+  novalidate: true,
+  open: true,
+  playsinline: true,
+  readonly: true,
+  required: true,
+  reversed: true,
+  selected: true,
+  truespeed: true,
+  webkitdirectory: true,
+};
+
+function truthy(val) {
+  return val === "true" || val === true;
+}

+ 753 - 0
packages/interpreter/src/interpreter.js

@@ -0,0 +1,753 @@
+// this handler is only provided on the desktop and liveview implementations since this
+// method is not used by the web implementation
+this.handler = async function (event, name, bubbles) {
+  let target = event.target;
+  if (target != null) {
+    let preventDefaultRequests = null;
+    // Some events can be triggered on text nodes, which don't have attributes
+    if (target instanceof Element) {
+      preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`);
+    }
+
+    if (event.type === "click") {
+      // todo call prevent default if it's the right type of event
+      if (intercept_link_redirects) {
+        let a_element = target.closest("a");
+        if (a_element != null) {
+          event.preventDefault();
+
+          let elementShouldPreventDefault =
+            preventDefaultRequests && preventDefaultRequests.includes(`onclick`);
+          let aElementShouldPreventDefault = a_element.getAttribute(
+            `dioxus-prevent-default`
+          );
+          let linkShouldPreventDefault =
+            aElementShouldPreventDefault &&
+            aElementShouldPreventDefault.includes(`onclick`);
+
+          if (!elementShouldPreventDefault && !linkShouldPreventDefault) {
+            const href = a_element.getAttribute("href");
+            if (href !== "" && href !== null && href !== undefined) {
+              window.ipc.postMessage(
+                this.serializeIpcMessage("browser_open", { href })
+              );
+            }
+          }
+        }
+      }
+
+      // also prevent buttons from submitting
+      if (target.tagName === "BUTTON" && event.type == "submit") {
+        event.preventDefault();
+      }
+    }
+
+    const realId = find_real_id(target);
+
+    if (
+      preventDefaultRequests &&
+      preventDefaultRequests.includes(`on${event.type}`)
+    ) {
+      event.preventDefault();
+    }
+
+    if (event.type === "submit") {
+      event.preventDefault();
+    }
+
+    let contents = await serialize_event(event);
+
+    // TODO: this should be liveview only
+    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 = window.interpreter.serializeIpcMessage("user_event", {
+            name: name,
+            element: parseInt(realId),
+            data: contents,
+            bubbles,
+          });
+          window.ipc.postMessage(message);
+        }
+        read_files();
+        return;
+      }
+    }
+
+    if (
+      target.tagName === "FORM" &&
+      (event.type === "submit" || event.type === "input")
+    ) {
+      const formData = new FormData(target);
+
+      for (let name of formData.keys()) {
+        const fieldType = target.elements[name].type;
+
+        switch (fieldType) {
+          case "select-multiple":
+            contents.values[name] = formData.getAll(name);
+            break;
+
+          // add cases for fieldTypes that can hold multiple values here
+          default:
+            contents.values[name] = formData.get(name);
+            break;
+        }
+      }
+    }
+
+    if (
+      target.tagName === "SELECT" &&
+      event.type === "input"
+    ) {
+      const selectData = target.options;
+      contents.values["options"] = [];
+      for (let i = 0; i < selectData.length; i++) {
+        let option = selectData[i];
+        if (option.selected) {
+          contents.values["options"].push(option.value.toString());
+        }
+      }
+    }
+
+    if (realId === null) {
+      return;
+    }
+    window.ipc.postMessage(
+      this.serializeIpcMessage("user_event", {
+        name: name,
+        element: parseInt(realId),
+        data: contents,
+        bubbles,
+      })
+    );
+  }
+}
+
+function find_real_id(target) {
+  let realId = null;
+  if (target instanceof Element) {
+    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;
+    if (target instanceof Element) {
+      realId = target.getAttribute(`data-dioxus-id`);
+    }
+  }
+  return realId;
+}
+
+class ListenerMap {
+  constructor(root) {
+    // bubbling events can listen at the root element
+    this.global = {};
+    // non bubbling events listen at the element the listener was created at
+    this.local = {};
+    this.root = null;
+  }
+
+  create(event_name, element, bubbles, handler) {
+    if (bubbles) {
+      if (this.global[event_name] === undefined) {
+        this.global[event_name] = {};
+        this.global[event_name].active = 1;
+        this.root.addEventListener(event_name, 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, handler);
+    }
+  }
+
+  remove(element, event_name, bubbles) {
+    if (bubbles) {
+      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];
+      }
+    }
+    else {
+      const id = element.getAttribute("data-dioxus-id");
+      delete this.local[id][event_name];
+      if (this.local[id].length === 0) {
+        delete this.local[id];
+      }
+      element.removeEventListener(event_name, this.global[event_name].callback);
+    }
+  }
+
+  removeAllNonBubbling(element) {
+    const id = element.getAttribute("data-dioxus-id");
+    delete this.local[id];
+  }
+}
+this.LoadChild = function (array) {
+  // iterate through each number and get that child
+  let node = this.stack[this.stack.length - 1];
+
+  for (let i = 0; i < array.length; i++) {
+    this.end = array[i];
+    for (node = node.firstChild; this.end > 0; this.end--) {
+      node = node.nextSibling;
+    }
+  }
+  return node;
+}
+this.listeners = new ListenerMap();
+this.nodes = [];
+this.stack = [];
+this.root;
+this.templates = {};
+this.els = null;
+this.end = null;
+
+this.AppendChildren = function (id, many) {
+  this.root = this.nodes[id];
+  this.els = this.stack.splice(this.stack.length - many);
+  for (let k = 0; k < many; k++) {
+    this.root.appendChild(this.els[k]);
+  }
+}
+
+this.initialize = function (root) {
+  this.nodes = [root];
+  this.stack = [root];
+  this.listeners.root = root;
+}
+
+this.getClientRect = function (id) {
+  const node = this.nodes[id];
+  if (!node) {
+    return;
+  }
+  const rect = node.getBoundingClientRect();
+  return {
+    type: "GetClientRect",
+    origin: [rect.x, rect.y],
+    size: [rect.width, rect.height],
+  };
+}
+
+this.scrollTo = function (id, behavior) {
+  const node = this.nodes[id];
+  if (!node) {
+    return false;
+  }
+  node.scrollIntoView({
+    behavior: behavior,
+  });
+  return true;
+}
+
+/// Set the focus on the element
+this.setFocus = function (id, focus) {
+  const node = this.nodes[id];
+  if (!node) {
+    return false;
+  }
+  if (focus) {
+    node.focus();
+  } else {
+    node.blur();
+  }
+  return true;
+}
+
+function get_mouse_data(event) {
+  const {
+    altKey,
+    button,
+    buttons,
+    clientX,
+    clientY,
+    ctrlKey,
+    metaKey,
+    offsetX,
+    offsetY,
+    pageX,
+    pageY,
+    screenX,
+    screenY,
+    shiftKey,
+  } = event;
+  return {
+    alt_key: altKey,
+    button: button,
+    buttons: buttons,
+    client_x: clientX,
+    client_y: clientY,
+    ctrl_key: ctrlKey,
+    meta_key: metaKey,
+    offset_x: offsetX,
+    offset_y: offsetY,
+    page_x: pageX,
+    page_y: pageY,
+    screen_x: screenX,
+    screen_y: screenY,
+    shift_key: shiftKey,
+  };
+}
+
+async function serialize_event(event) {
+  switch (event.type) {
+    case "copy":
+    case "cut":
+    case "past": {
+      return {};
+    }
+    case "compositionend":
+    case "compositionstart":
+    case "compositionupdate": {
+      let { data } = event;
+      return {
+        data,
+      };
+    }
+    case "keydown":
+    case "keypress":
+    case "keyup": {
+      let {
+        charCode,
+        isComposing,
+        key,
+        altKey,
+        ctrlKey,
+        metaKey,
+        keyCode,
+        shiftKey,
+        location,
+        repeat,
+        which,
+        code,
+      } = event;
+      return {
+        char_code: charCode,
+        is_composing: isComposing,
+        key: key,
+        alt_key: altKey,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        key_code: keyCode,
+        shift_key: shiftKey,
+        location: location,
+        repeat: repeat,
+        which: which,
+        code,
+      };
+    }
+    case "focus":
+    case "blur": {
+      return {};
+    }
+    case "change": {
+      let target = event.target;
+      let value;
+      if (target.type === "checkbox" || target.type === "radio") {
+        value = target.checked ? "true" : "false";
+      } else {
+        value = target.value ?? target.textContent;
+      }
+      return {
+        value: value,
+        values: {},
+      };
+    }
+    case "input":
+    case "invalid":
+    case "reset":
+    case "submit": {
+      let target = event.target;
+      let value = target.value ?? target.textContent;
+
+      if (target.type === "checkbox") {
+        value = target.checked ? "true" : "false";
+      }
+
+      return {
+        value: value,
+        values: {},
+      };
+    }
+    case "drag":
+    case "dragend":
+    case "dragenter":
+    case "dragexit":
+    case "dragleave":
+    case "dragover":
+    case "dragstart":
+    case "drop": {
+      let files = null;
+      if (event.dataTransfer && event.dataTransfer.files) {
+        files = await serializeFileList(event.dataTransfer.files);
+      }
+
+      return { mouse: get_mouse_data(event), files };
+    }
+    case "click":
+    case "contextmenu":
+    case "doubleclick":
+    case "dblclick":
+    case "mousedown":
+    case "mouseenter":
+    case "mouseleave":
+    case "mousemove":
+    case "mouseout":
+    case "mouseover":
+    case "mouseup": {
+      return get_mouse_data(event);
+    }
+    case "pointerdown":
+    case "pointermove":
+    case "pointerup":
+    case "pointercancel":
+    case "gotpointercapture":
+    case "lostpointercapture":
+    case "pointerenter":
+    case "pointerleave":
+    case "pointerover":
+    case "pointerout": {
+      const {
+        altKey,
+        button,
+        buttons,
+        clientX,
+        clientY,
+        ctrlKey,
+        metaKey,
+        pageX,
+        pageY,
+        screenX,
+        screenY,
+        shiftKey,
+        pointerId,
+        width,
+        height,
+        pressure,
+        tangentialPressure,
+        tiltX,
+        tiltY,
+        twist,
+        pointerType,
+        isPrimary,
+      } = event;
+      return {
+        alt_key: altKey,
+        button: button,
+        buttons: buttons,
+        client_x: clientX,
+        client_y: clientY,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        page_x: pageX,
+        page_y: pageY,
+        screen_x: screenX,
+        screen_y: screenY,
+        shift_key: shiftKey,
+        pointer_id: pointerId,
+        width: width,
+        height: height,
+        pressure: pressure,
+        tangential_pressure: tangentialPressure,
+        tilt_x: tiltX,
+        tilt_y: tiltY,
+        twist: twist,
+        pointer_type: pointerType,
+        is_primary: isPrimary,
+      };
+    }
+    case "select": {
+      return {};
+    }
+    case "touchcancel":
+    case "touchend":
+    case "touchmove":
+    case "touchstart": {
+      const { altKey, ctrlKey, metaKey, shiftKey } = event;
+      return {
+        // changed_touches: event.changedTouches,
+        // target_touches: event.targetTouches,
+        // touches: event.touches,
+        alt_key: altKey,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        shift_key: shiftKey,
+      };
+    }
+    case "scroll": {
+      return {};
+    }
+    case "wheel": {
+      const { deltaX, deltaY, deltaZ, deltaMode } = event;
+      return {
+        delta_x: deltaX,
+        delta_y: deltaY,
+        delta_z: deltaZ,
+        delta_mode: deltaMode,
+      };
+    }
+    case "animationstart":
+    case "animationend":
+    case "animationiteration": {
+      const { animationName, elapsedTime, pseudoElement } = event;
+      return {
+        animation_name: animationName,
+        elapsed_time: elapsedTime,
+        pseudo_element: pseudoElement,
+      };
+    }
+    case "transitionend": {
+      const { propertyName, elapsedTime, pseudoElement } = event;
+      return {
+        property_name: propertyName,
+        elapsed_time: elapsedTime,
+        pseudo_element: pseudoElement,
+      };
+    }
+    case "abort":
+    case "canplay":
+    case "canplaythrough":
+    case "durationchange":
+    case "emptied":
+    case "encrypted":
+    case "ended":
+    case "error":
+    case "loadeddata":
+    case "loadedmetadata":
+    case "loadstart":
+    case "pause":
+    case "play":
+    case "playing":
+    case "progress":
+    case "ratechange":
+    case "seeked":
+    case "seeking":
+    case "stalled":
+    case "suspend":
+    case "timeupdate":
+    case "volumechange":
+    case "waiting": {
+      return {};
+    }
+    case "toggle": {
+      return {};
+    }
+    default: {
+      return {};
+    }
+  }
+}
+this.serializeIpcMessage = function (method, params = {}) {
+  return JSON.stringify({ method, params });
+}
+
+function is_element_node(node) {
+  return node.nodeType == 1;
+}
+
+function event_bubbles(event) {
+  switch (event) {
+    case "copy":
+      return true;
+    case "cut":
+      return true;
+    case "paste":
+      return true;
+    case "compositionend":
+      return true;
+    case "compositionstart":
+      return true;
+    case "compositionupdate":
+      return true;
+    case "keydown":
+      return true;
+    case "keypress":
+      return true;
+    case "keyup":
+      return true;
+    case "focus":
+      return false;
+    case "focusout":
+      return true;
+    case "focusin":
+      return true;
+    case "blur":
+      return false;
+    case "change":
+      return true;
+    case "input":
+      return true;
+    case "invalid":
+      return true;
+    case "reset":
+      return true;
+    case "submit":
+      return true;
+    case "click":
+      return true;
+    case "contextmenu":
+      return true;
+    case "doubleclick":
+      return true;
+    case "dblclick":
+      return true;
+    case "drag":
+      return true;
+    case "dragend":
+      return true;
+    case "dragenter":
+      return false;
+    case "dragexit":
+      return false;
+    case "dragleave":
+      return true;
+    case "dragover":
+      return true;
+    case "dragstart":
+      return true;
+    case "drop":
+      return true;
+    case "mousedown":
+      return true;
+    case "mouseenter":
+      return false;
+    case "mouseleave":
+      return false;
+    case "mousemove":
+      return true;
+    case "mouseout":
+      return true;
+    case "scroll":
+      return false;
+    case "mouseover":
+      return true;
+    case "mouseup":
+      return true;
+    case "pointerdown":
+      return true;
+    case "pointermove":
+      return true;
+    case "pointerup":
+      return true;
+    case "pointercancel":
+      return true;
+    case "gotpointercapture":
+      return true;
+    case "lostpointercapture":
+      return true;
+    case "pointerenter":
+      return false;
+    case "pointerleave":
+      return false;
+    case "pointerover":
+      return true;
+    case "pointerout":
+      return true;
+    case "select":
+      return true;
+    case "touchcancel":
+      return true;
+    case "touchend":
+      return true;
+    case "touchmove":
+      return true;
+    case "touchstart":
+      return true;
+    case "wheel":
+      return true;
+    case "abort":
+      return false;
+    case "canplay":
+      return false;
+    case "canplaythrough":
+      return false;
+    case "durationchange":
+      return false;
+    case "emptied":
+      return false;
+    case "encrypted":
+      return true;
+    case "ended":
+      return false;
+    case "error":
+      return false;
+    case "loadeddata":
+    case "loadedmetadata":
+    case "loadstart":
+    case "load":
+      return false;
+    case "pause":
+      return false;
+    case "play":
+      return false;
+    case "playing":
+      return false;
+    case "progress":
+      return false;
+    case "ratechange":
+      return false;
+    case "seeked":
+      return false;
+    case "seeking":
+      return false;
+    case "stalled":
+      return false;
+    case "suspend":
+      return false;
+    case "timeupdate":
+      return false;
+    case "volumechange":
+      return false;
+    case "waiting":
+      return false;
+    case "animationstart":
+      return true;
+    case "animationend":
+      return true;
+    case "animationiteration":
+      return true;
+    case "transitionend":
+      return true;
+    case "toggle":
+      return true;
+    case "mounted":
+      return false;
+  }
+
+  return true;
+}

+ 1 - 1
packages/interpreter/src/lib.rs

@@ -19,7 +19,7 @@ pub mod minimal_bindings {
     /// Some useful snippets that we use to share common functionality between the different platforms we support.
     ///
     /// This maintains some sort of consistency between web, desktop, and liveview
-    #[wasm_bindgen(module = "/src/gen/common.js")]
+    #[wasm_bindgen(module = "/src/common_exported.js")]
     extern "C" {
         /// Set the attribute of the node
         pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>);

+ 418 - 0
packages/interpreter/src/sledgehammer_bindings.rs

@@ -0,0 +1,418 @@
+#[cfg(feature = "webonly")]
+use js_sys::Function;
+#[cfg(feature = "webonly")]
+use sledgehammer_bindgen::bindgen;
+#[cfg(feature = "webonly")]
+use web_sys::Node;
+
+#[cfg(feature = "webonly")]
+pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
+
+#[cfg(feature = "webonly")]
+#[bindgen(module)]
+mod js {
+    const JS_FILE: &str = "./src/common.js";
+    const JS: &str = r#"
+    class ListenerMap {
+        constructor(root) {
+            // bubbling events can listen at the root element
+            this.global = {};
+            // non bubbling events listen at the element the listener was created at
+            this.local = {};
+            this.root = root;
+            this.handler = null;
+        }
+
+        create(event_name, element, bubbles) {
+            if (bubbles) {
+                if (this.global[event_name] === undefined) {
+                    this.global[event_name] = {};
+                    this.global[event_name].active = 1;
+                    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);
+            }
+        }
+
+        remove(element, event_name, bubbles) {
+            if (bubbles) {
+                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];
+                }
+            }
+            else {
+                const id = element.getAttribute("data-dioxus-id");
+                delete this.local[id][event_name];
+                if (this.local[id].length === 0) {
+                    delete this.local[id];
+                }
+                element.removeEventListener(event_name, this.handler);
+            }
+        }
+
+        removeAllNonBubbling(element) {
+            const id = element.getAttribute("data-dioxus-id");
+            delete this.local[id];
+        }
+    }
+    this.LoadChild = function(ptr, len) {
+        // iterate through each number and get that child
+        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;
+    }
+    this.listeners = new ListenerMap();
+    this.nodes = [];
+    this.stack = [];
+    this.root = null;
+    this.templates = {};
+    this.els = null;
+    this.save_template = function(nodes, tmpl_id) {
+        this.templates[tmpl_id] = nodes;
+    }
+    this.hydrate = function (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);
+                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.listeners.create(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();
+        }
+    }
+    this.get_node = function(id) {
+        return this.nodes[id];
+    }
+    this.initialize = function(root, handler) {
+        this.listeners.handler = handler;
+        this.nodes = [root];
+        this.stack = [root];
+        this.listeners.root = root;
+    }
+    this.AppendChildren = function (id, many){
+        let root = this.nodes[id];
+        this.els = this.stack.splice(this.stack.length-many);
+        for (let k = 0; k < many; k++) {
+            root.appendChild(this.els[k]);
+        }
+    }
+    "#;
+
+    fn mount_to_root() {
+        "{this.AppendChildren(this.root, this.stack.length-1);}"
+    }
+    fn push_root(root: u32) {
+        "{this.stack.push(this.nodes[$root$]);}"
+    }
+    fn append_children(id: u32, many: u16) {
+        "{this.AppendChildren($id$, $many$);}"
+    }
+    fn pop_root() {
+        "{this.stack.pop();}"
+    }
+    fn replace_with(id: u32, n: u16) {
+        "{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...this.els);}"
+    }
+    fn insert_after(id: u32, n: u16) {
+        "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
+    }
+    fn insert_before(id: u32, n: u16) {
+        "{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}"
+    }
+    fn remove(id: u32) {
+        "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.listeners.removeAllNonBubbling(node); } node.remove(); }}"
+    }
+    fn create_raw_text(text: &str) {
+        "{this.stack.push(document.createTextNode($text$));}"
+    }
+    fn create_text_node(text: &str, id: u32) {
+        "{let node = document.createTextNode($text$); this.nodes[$id$] = node; this.stack.push(node);}"
+    }
+    fn create_placeholder(id: u32) {
+        "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}"
+    }
+    fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
+        r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.listeners.create($event_name$, node, $bubbles$);"#
+    }
+    fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
+        "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.listeners.remove(node, $event_name$, $bubbles$);}"
+    }
+    fn set_text(id: u32, text: &str) {
+        "{this.nodes[$id$].textContent = $text$;}"
+    }
+    fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
+        "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}"
+    }
+    fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
+        r#"{
+            let node = this.nodes[$id$];
+            if (!ns) {
+                switch (field) {
+                    case "value":
+                        node.value = "";
+                        break;
+                    case "checked":
+                        node.checked = false;
+                        break;
+                    case "selected":
+                        node.selected = false;
+                        break;
+                    case "dangerous_inner_html":
+                        node.innerHTML = "";
+                        break;
+                    default:
+                        node.removeAttribute(field);
+                        break;
+                }
+            } else if (ns == "style") {
+                node.style.removeProperty(name);
+            } else {
+                node.removeAttributeNS(ns, field);
+            }
+        }"#
+    }
+    fn assign_id(ptr: u32, len: u8, id: u32) {
+        "{this.nodes[$id$] = this.LoadChild($ptr$, $len$);}"
+    }
+    fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) {
+        r#"{
+            let node = this.LoadChild($ptr$, $len$);
+            if (node.nodeType == node.TEXT_NODE) {
+                node.textContent = value;
+            } else {
+                let text = document.createTextNode(value);
+                node.replaceWith(text);
+                node = text;
+            }
+            this.nodes[$id$] = node;
+        }"#
+    }
+    fn replace_placeholder(ptr: u32, len: u8, n: u16) {
+        "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($ptr$, $len$); node.replaceWith(...this.els);}"
+    }
+    fn load_template(tmpl_id: u16, index: u16, id: u32) {
+        "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}"
+    }
+}
+
+#[cfg(feature = "webonly")]
+#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
+export function save_template(channel, nodes, tmpl_id) {
+    channel.save_template(nodes, tmpl_id);
+}
+export function hydrate(channel, ids) {
+    channel.hydrate(ids);
+}
+export function get_node(channel, id) {
+    return channel.get_node(id);
+}
+export function initialize(channel, root, handler) {
+    channel.initialize(root, handler);
+}
+"#)]
+extern "C" {
+    pub fn save_template(channel: &JSChannel, nodes: Vec<Node>, tmpl_id: u16);
+
+    pub fn hydrate(channel: &JSChannel, ids: Vec<u32>);
+
+    pub fn get_node(channel: &JSChannel, id: u32) -> Node;
+
+    pub fn initialize(channel: &JSChannel, root: Node, handler: &Function);
+}
+
+#[cfg(feature = "binary-protocol")]
+pub mod binary_protocol {
+    use sledgehammer_bindgen::bindgen;
+    pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
+
+    #[bindgen]
+    mod protocol_js {
+        const JS_FILE: &str = "./src/interpreter.js";
+        const JS_FILE: &str = "./src/common.js";
+
+        fn mount_to_root() {
+            "{this.AppendChildren(this.root, this.stack.length-1);}"
+        }
+        fn push_root(root: u32) {
+            "{this.stack.push(this.nodes[$root$]);}"
+        }
+        fn append_children(id: u32, many: u16) {
+            "{this.AppendChildren($id$, $many$);}"
+        }
+        fn append_children_to_top(many: u16) {
+            "{
+                let root = this.stack[this.stack.length-many-1];
+                this.els = this.stack.splice(this.stack.length-many);
+                for (let k = 0; k < many; k++) {
+                    root.appendChild(this.els[k]);
+                }
+            }"
+        }
+        fn pop_root() {
+            "{this.stack.pop();}"
+        }
+        fn replace_with(id: u32, n: u16) {
+            "{let root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...this.els);}"
+        }
+        fn insert_after(id: u32, n: u16) {
+            "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
+        }
+        fn insert_before(id: u32, n: u16) {
+            "{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}"
+        }
+        fn remove(id: u32) {
+            "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.listeners.removeAllNonBubbling(node); } node.remove(); }}"
+        }
+        fn create_raw_text(text: &str) {
+            "{this.stack.push(document.createTextNode($text$));}"
+        }
+        fn create_text_node(text: &str, id: u32) {
+            "{let node = document.createTextNode($text$); this.nodes[$id$] = node; this.stack.push(node);}"
+        }
+        fn create_element(element: &'static str<u8, el>) {
+            "{this.stack.push(document.createElement($element$))}"
+        }
+        fn create_element_ns(element: &'static str<u8, el>, ns: &'static str<u8, namespace>) {
+            "{this.stack.push(document.createElementNS($ns$, $element$))}"
+        }
+        fn create_placeholder(id: u32) {
+            "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}"
+        }
+        fn add_placeholder() {
+            "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node);}"
+        }
+        fn new_event_listener(event: &str<u8, evt>, id: u32, bubbles: u8) {
+            r#"
+            bubbles = bubbles == 1;
+            let node = this.nodes[id];
+            if(node.listening){
+                node.listening += 1;
+            } else {
+                node.listening = 1;
+            }
+            node.setAttribute('data-dioxus-id', `\${id}`);
+            const event_name = $event$;
+
+            // if this is a mounted listener, we send the event immediately
+            if (event_name === "mounted") {
+                window.ipc.postMessage(
+                    this.serializeIpcMessage("user_event", {
+                        name: event_name,
+                        element: id,
+                        data: null,
+                        bubbles,
+                    })
+                );
+            } else {
+                this.listeners.create(event_name, node, bubbles, (event) => {
+                    this.handler(event, event_name, bubbles);
+                });
+            }"#
+        }
+        fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
+            "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.listeners.remove(node, $event_name$, $bubbles$);}"
+        }
+        fn set_text(id: u32, text: &str) {
+            "{this.nodes[$id$].textContent = $text$;}"
+        }
+        fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
+            "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}"
+        }
+        fn set_top_attribute(field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
+            "{this.setAttributeInner(this.stack[this.stack.length-1], $field$, $value$, $ns$);}"
+        }
+        fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
+            r#"{
+                let node = this.nodes[$id$];
+                if (!ns) {
+                    switch (field) {
+                        case "value":
+                            node.value = "";
+                            break;
+                        case "checked":
+                            node.checked = false;
+                            break;
+                        case "selected":
+                            node.selected = false;
+                            break;
+                        case "dangerous_inner_html":
+                            node.innerHTML = "";
+                            break;
+                        default:
+                            node.removeAttribute(field);
+                            break;
+                    }
+                } else if (ns == "style") {
+                    node.style.removeProperty(name);
+                } else {
+                    node.removeAttributeNS(ns, field);
+                }
+            }"#
+        }
+        fn assign_id(array: &[u8], id: u32) {
+            "{this.nodes[$id$] = this.LoadChild($array$);}"
+        }
+        fn hydrate_text(array: &[u8], value: &str, id: u32) {
+            r#"{
+                let node = this.LoadChild($array$);
+                if (node.nodeType == node.TEXT_NODE) {
+                    node.textContent = value;
+                } else {
+                    let text = document.createTextNode(value);
+                    node.replaceWith(text);
+                    node = text;
+                }
+                this.nodes[$id$] = node;
+            }"#
+        }
+        fn replace_placeholder(array: &[u8], n: u16) {
+            "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...this.els);}"
+        }
+        fn load_template(tmpl_id: u16, index: u16, id: u32) {
+            "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}"
+        }
+        fn add_templates(tmpl_id: u16, len: u16) {
+            "{this.templates[$tmpl_id$] = this.stack.splice(this.stack.length-$len$);}"
+        }
+    }
+}

+ 3 - 2
packages/liveview/src/main.js

@@ -1,4 +1,4 @@
-const config = new InterpreterConfig(false);
+const intercept_link_redirects = false;
 
 function main() {
   let root = window.document.getElementById("main");
@@ -9,6 +9,7 @@ function main() {
 
 class IPC {
   constructor(root) {
+    window.interpreter = new JSChannel();
     window.interpreter.initialize(root);
     const ws = new WebSocket(WS_ADDR);
     ws.binaryType = "arraybuffer";
@@ -34,7 +35,7 @@ class IPC {
       // The first byte tells the shim if this is a binary of text frame
       if (binaryFrame) {
         // binary frame
-        run_from_bytes(messageData);
+        window.interpreter.run_from_bytes(messageData);
       }
       else {
         // text frame

+ 1 - 0
packages/router-macro/src/lib.rs

@@ -559,6 +559,7 @@ impl RouteEnum {
             #(#type_defs)*
 
             #[allow(non_camel_case_types)]
+            #[allow(clippy::derive_partial_eq_without_eq)]
             #[derive(Debug, PartialEq)]
             pub enum #match_error_name {
                 #(#error_variants),*

+ 1 - 0
packages/router-macro/src/segment.rs

@@ -309,6 +309,7 @@ pub(crate) fn create_error_type(
 
     quote! {
         #[allow(non_camel_case_types)]
+        #[allow(clippy::derive_partial_eq_without_eq)]
         #[derive(Debug, PartialEq)]
         pub enum #error_name {
             ExtraSegments(String),

+ 1 - 0
packages/web/src/dom.rs

@@ -108,6 +108,7 @@ impl WebsysDom {
         }));
 
         dioxus_interpreter_js::initialize(
+            interpreter.js_channel(),
             root.clone().unchecked_into(),
             handler.as_ref().unchecked_ref(),
         );

+ 2 - 2
packages/web/src/mutations.rs

@@ -62,7 +62,7 @@ impl WebsysDom {
         // Now that we've flushed the edits and the dom nodes exist, we can send the mounted events.
         {
             for id in self.queued_mounted_events.drain(..) {
-                let node = get_node(id.0 as u32);
+                let node = get_node(self.interpreter.js_channel(), id.0 as u32);
                 if let Some(element) = node.dyn_ref::<web_sys::Element>() {
                     let _ = self.event_channel.unbounded_send(UiEvent {
                         name: "mounted".to_string(),
@@ -91,7 +91,7 @@ impl WriteMutations for WebsysDom {
 
         self.templates
             .insert(template.name.to_owned(), self.max_template_id);
-        save_template(roots, self.max_template_id);
+        save_template(self.interpreter.js_channel(), roots, self.max_template_id);
         self.max_template_id += 1
     }
 

+ 6 - 2
packages/web/src/rehydrate.rs

@@ -23,7 +23,7 @@ impl WebsysDom {
         // Recursively rehydrate the dom from the VirtualDom
         self.rehydrate_scope(root_scope, dom, &mut ids, &mut to_mount)?;
 
-        dioxus_interpreter_js::hydrate(ids);
+        dioxus_interpreter_js::hydrate(self.interpreter.js_channel(), ids);
 
         #[cfg(feature = "mounted")]
         for id in to_mount {
@@ -168,7 +168,11 @@ impl WriteMutations for OnlyWriteTemplates<'_> {
         self.0
             .templates
             .insert(template.name.to_owned(), self.0.max_template_id);
-        save_template(roots, self.0.max_template_id);
+        save_template(
+            self.0.interpreter.js_channel(),
+            roots,
+            self.0.max_template_id,
+        );
         self.0.max_template_id += 1
     }