diff --git a/Cargo.lock b/Cargo.lock index 456f4bf7a..de7afb9eb 100644 --- a/Cargo.lock +++ b/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 = "9298e863f0143b89972299110a7fa3e2fc08c412341d588c497bae38409f9e68" +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 = "04559ded3de5c62f08457cadcb6c44649c4d90e72fdc0804c6c30ce1bc526304" dependencies = [ "quote", "syn 2.0.50", diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index e49d911f9..e9d4125b2 100644 --- a/packages/desktop/src/protocol.rs +++ b/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"); diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 009873a6e..ba813a538 100644 --- a/packages/interpreter/Cargo.toml +++ b/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 } diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.js new file mode 100644 index 000000000..055353fa5 --- /dev/null +++ b/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; +} diff --git a/packages/interpreter/src/common_exported.js b/packages/interpreter/src/common_exported.js new file mode 100644 index 000000000..0c64085f9 --- /dev/null +++ b/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; +} diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js new file mode 100644 index 000000000..5887828c7 --- /dev/null +++ b/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; +} diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index 304cc2ed3..685e51af3 100644 --- a/packages/interpreter/src/lib.rs +++ b/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>); diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs new file mode 100644 index 000000000..18e4b9df5 --- /dev/null +++ b/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, 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, 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, value: &str, ns: &str) { + "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}" + } + fn remove_attribute(id: u32, field: &str, ns: &str) { + 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, tmpl_id: u16); + + pub fn hydrate(channel: &JSChannel, ids: Vec); + + 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) { + "{this.stack.push(document.createElement($element$))}" + } + fn create_element_ns(element: &'static str, ns: &'static str) { + "{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, 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, 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, value: &str, ns: &str) { + "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}" + } + fn set_top_attribute(field: &str, value: &str, ns: &str) { + "{this.setAttributeInner(this.stack[this.stack.length-1], $field$, $value$, $ns$);}" + } + fn remove_attribute(id: u32, field: &str, ns: &str) { + 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$);}" + } + } +} diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 22267dbab..59a32dd0b 100644 --- a/packages/liveview/src/main.js +++ b/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 diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index 1f8e2371d..00a7499a4 100644 --- a/packages/router-macro/src/lib.rs +++ b/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),* diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index 21ebe5b60..f96bab431 100644 --- a/packages/router-macro/src/segment.rs +++ b/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), diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index ab3ea3ebf..428b8d8b9 100644 --- a/packages/web/src/dom.rs +++ b/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(), ); diff --git a/packages/web/src/mutations.rs b/packages/web/src/mutations.rs index feadb2995..c3c2a8147 100644 --- a/packages/web/src/mutations.rs +++ b/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::() { 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 } diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 7593e47d0..271b3daab 100644 --- a/packages/web/src/rehydrate.rs +++ b/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 }