diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index ee15f7ac7..a211c37c2 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -23,7 +23,7 @@ fn main() { fn gen_bindings(name: &str) { let contents = read_to_string(&format!("src/{name}.ts")).unwrap(); let generated = read_to_string(&format!("src/gen/{name}.js")).unwrap_or_default(); - let hashed = hash_file(&contents); + let hashed = hash_it(&contents); // If the file is generated, and the hash is the same, we're good, don't do anything if generated @@ -58,7 +58,7 @@ fn gen_bindings(name: &str) { std::fs::write(&format!("src/gen/{name}.js"), generated).unwrap(); } -fn hash_file(obj: &str) -> u64 { +fn hash_it(obj: impl Hash) -> u64 { let mut hasher = DefaultHasher::new(); obj.hash(&mut hasher); hasher.finish() diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.js deleted file mode 100644 index 055353fa5..000000000 --- a/packages/interpreter/src/common.js +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 0c64085f9..000000000 --- a/packages/interpreter/src/common_exported.js +++ /dev/null @@ -1,79 +0,0 @@ -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/gen/common.js b/packages/interpreter/src/gen/common.js deleted file mode 100644 index 8f21059f3..000000000 --- a/packages/interpreter/src/gen/common.js +++ /dev/null @@ -1,81 +0,0 @@ -// DO NOT EDIT THIS FILE. HASH: 9578489549991746027 -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/gen/form.js b/packages/interpreter/src/gen/form.js deleted file mode 100644 index 0357a8736..000000000 --- a/packages/interpreter/src/gen/form.js +++ /dev/null @@ -1 +0,0 @@ -// DO NOT EDIT THIS FILE. HASH: 3476900567878811119 diff --git a/packages/interpreter/src/gen/interpreter.js b/packages/interpreter/src/gen/interpreter.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/interpreter/src/gen/sledgehammer.js b/packages/interpreter/src/gen/sledgehammer.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 5887828c7..c3d63316e 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -145,609 +145,3 @@ this.handler = async function (event, name, 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/ts/form.ts b/packages/interpreter/src/ts/form.ts index c68723b09..7f4581309 100644 --- a/packages/interpreter/src/ts/form.ts +++ b/packages/interpreter/src/ts/form.ts @@ -1,3 +1,53 @@ -function retrieveFormValues() { +// Consistently deserialize forms and form elements for use across web/desktop/mobile +type FormValues = { [key: string]: FormDataEntryValue[] }; + +export function retriveValues(event: Event, target: HTMLElement): FormValues { + const contents: FormValues = {}; + + if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) { + retrieveFormValues(target, contents); + } + + if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) { + retriveInputsValues(target, contents); + } + + return contents; +} + +export function retrieveFormValues(form: HTMLFormElement, contents: FormValues) { + const formData = new FormData(form); + + for (let name in formData.keys()) { + let element = form.elements.namedItem(name); + + // todo: this is going to be a problem for select-multiple? + if (!(element instanceof HTMLInputElement)) { + continue; + } + + switch (element.type) { + case "select-multiple": + contents[name] = formData.getAll(name); + break; + + // By default, it's just a single value + default: + contents[name] = [formData.get(name)]; + break; + } + } +} + +export function retriveInputsValues(target: HTMLSelectElement, contents: FormValues,) { + const selectData = target.options; + contents["options"] = []; + + for (let i = 0; i < selectData.length; i++) { + let option = selectData[i]; + if (option.selected) { + contents["options"].push(option.value.toString()); + } + } } diff --git a/packages/interpreter/src/ts/interpreter_core.ts b/packages/interpreter/src/ts/interpreter_core.ts index d13d71f3d..557dab3e5 100644 --- a/packages/interpreter/src/ts/interpreter_core.ts +++ b/packages/interpreter/src/ts/interpreter_core.ts @@ -1,9 +1,6 @@ // The root interpreter class that holds state about the mapping between DOM and VirtualDom // This always lives in the JS side of things, and is extended by the native and web interpreters -// todo: we want to make these vars that the interpreter uses to be isolated so we can have multiple interpreters -export let m: DataView; // p, ls, d, t, op, i, e, z, metaflags; - export class Interpreter { // non bubbling events listen at the element the listener was created at global: { @@ -15,15 +12,15 @@ export class Interpreter { [key: string]: EventListener } }; - root: Element; + root: HTMLElement; handler: EventListener; - nodes: Element[]; - stack: Element[]; + nodes: Node[]; + stack: Node[]; templates: { - [key: string]: Element[] + [key: string]: Node[] }; - constructor(root: Element, handler: EventListener) { + constructor(root: HTMLElement, handler: EventListener) { this.root = root; this.nodes = [root]; this.stack = [root]; @@ -32,7 +29,7 @@ export class Interpreter { this.handler = handler; } - createListener(event_name: string, element: Element, bubbles: boolean) { + createListener(event_name: string, element: HTMLElement, bubbles: boolean) { if (bubbles) { if (this.global[event_name] === undefined) { this.global[event_name] = { active: 1, callback: this.handler }; @@ -49,7 +46,7 @@ export class Interpreter { } } - removeListener(element: Element, event_name: string, bubbles: boolean) { + removeListener(element: HTMLElement, event_name: string, bubbles: boolean) { if (bubbles) { this.removeBubblingListener(event_name); } else { @@ -65,7 +62,7 @@ export class Interpreter { } } - removeNonBubblingListener(element: Element, event_name: string) { + removeNonBubblingListener(element: HTMLElement, event_name: string) { const id = element.getAttribute("data-dioxus-id"); delete this.local[id][event_name]; if (Object.keys(this.local[id]).length === 0) { @@ -74,7 +71,7 @@ export class Interpreter { element.removeEventListener(event_name, this.handler); } - removeAllNonBubblingListeners(element: Element) { + removeAllNonBubblingListeners(element: HTMLElement) { const id = element.getAttribute("data-dioxus-id"); delete this.local[id]; } diff --git a/packages/interpreter/src/ts/interpreter_native.ts b/packages/interpreter/src/ts/interpreter_native.ts index 13bb84ee4..e37cc3427 100644 --- a/packages/interpreter/src/ts/interpreter_native.ts +++ b/packages/interpreter/src/ts/interpreter_native.ts @@ -1,14 +1,27 @@ // This file provides an extended variant of the interpreter used for desktop and liveview interaction // -// +// This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't +// provide since it doesn't have access to the dom. +import { retriveValues } from "./form"; import { Interpreter } from "./interpreter_core"; +import { SerializedEvent, serializeEvent } from "./serialize"; export class NativeInterpreter extends Interpreter { intercept_link_redirects: boolean; + ipc: any; - constructor(root: Element) { - super(root, (event) => handler(event, this, event.type, true)); + // 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)); + this.intercept_link_redirects = true; + this.liveview = false; + + // @ts-ignore - wry gives us this + this.ipc = window.ipc; } serializeIpcMessage(method: string, params = {}) { @@ -17,50 +30,38 @@ export class NativeInterpreter extends Interpreter { scrollTo(id: number, behavior: ScrollBehavior) { const node = this.nodes[id]; - - if (!(node instanceof HTMLElement)) { - return false; + if (node instanceof HTMLElement) { + node.scrollIntoView({ behavior }); } - - node.scrollIntoView({ - behavior: behavior, - }); - - return true; } getClientRect(id: number) { const node = this.nodes[id]; - if (!node) { - return; + if (node instanceof HTMLElement) { + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [rect.x, rect.y], + size: [rect.width, rect.height], + }; } - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; } setFocus(id: number, focus: boolean) { const node = this.nodes[id]; - if (!(node instanceof HTMLElement)) { - return false; + if (node instanceof HTMLElement) { + if (focus) { + node.focus(); + } else { + node.blur(); + } } - - if (focus) { - node.focus(); - } else { - node.blur(); - } - - return true; } LoadChild(array: number[]) { // iterate through each number and get that child - let node = this.stack[this.stack.length - 1] as Node; + let node = this.stack[this.stack.length - 1]; for (let i = 0; i < array.length; i++) { let end = array[i]; @@ -80,73 +81,162 @@ export class NativeInterpreter extends Interpreter { root.appendChild(els[k]); } } -} + + handleEvent(event: Event, name: string, bubbles: boolean) { + const target = event.target!; + const realId = targetId(target)!; + const contents = serializeEvent(event); + + // Attempt to retrive the values from the form and inputs + if (target instanceof HTMLElement) { + contents.values = retriveValues(event, target); + } + + // Handle the event on the virtualdom and then preventDefault if it also preventsDefault + // Some listeners + let body = { + name: name, + data: contents, + element: realId, + bubbles, + }; + + // Run any prevent defaults the user might've set + // This is to support the prevent_default: "onclick" attribute that dioxus has had for a while, but is not necessary + // now that we expose preventDefault to the virtualdom on desktop + // Liveview will still need to use this + this.preventDefaults(event, target); + // liveview does not have syncronous event handling, so we need to send the event to the host + if (this.liveview) { + // Okay, so the user might've requested some files to be read + if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { + if (target.getAttribute("type") === "file") { + this.readFiles(target, contents, bubbles, realId, name); + } + } + } else { + // Run the event handler on the virtualdom + // capture/prevent default of the event if the virtualdom wants to + const res = handleVirtualdomEventSync(JSON.stringify(body)); -function handler(event: Event, interpreter: NativeInterpreter, name: string, bubbles: boolean) { - const target = event.target!; - const realId = target_id(target)!; - let contents = serializeEvent(event); + if (res.preventDefault) { + event.preventDefault(); + } - if (target instanceof HTMLFormElement && (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 (res.stopPropagation) { + event.stopPropagation(); } } } - if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) { - 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()); - } + async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: number, name: string) { + let files = target.files!; + let file_contents: { [name: string]: number[] } = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + + contents.files = { files: file_contents }; + + const message = this.serializeIpcMessage("user_event", { + name: name, + element: realId, + data: contents, + bubbles, + }); + + this.ipc.postMessage(message); + } + + + // This should: + // - prevent form submissions from navigating + // - prevent anchor tags from navigating + // - prevent buttons from submitting forms + // - let the virtualdom attempt to prevent the event + preventDefaults(event: Event, target: EventTarget) { + let preventDefaultRequests: string | null = 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 (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { + event.preventDefault(); + } + + if (event.type === "submit") { + event.preventDefault(); + } + + // Attempt to intercept if the event is a click + if (target instanceof Element && event.type === "click") { + this.handleClickNavigate(event, target, preventDefaultRequests); } } - // If there's files to read - if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { - if (target.getAttribute("type") === "file") { - read_files(target, contents, bubbles, realId, name); + handleClickNavigate(event: Event, target: Element, preventDefaultRequests: string) { + // todo call prevent default if it's the right type of event + if (!this.intercept_link_redirects) { return; } - } - prevents_default(event, target); + // prevent buttons in forms from submitting the form + if (target.tagName === "BUTTON" && event.type == "submit") { + event.preventDefault(); + } + // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating + let a_element = target.closest("a"); + if (a_element == null) { + return; + } - // Handle the event on the virtualdom and then process whatever its output was - let body = { - name: name, - data: serializeEvent(event), - element: parseInt(realId), - bubbles, - }; - - if (waitForVirtualDomPreventDefault(JSON.stringify(body))) { 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) { + this.ipc.postMessage( + this.serializeIpcMessage("browser_open", { href }) + ); + } + } } } -export function waitForVirtualDomPreventDefault(contents: string): boolean { - +type EventSyncResult = { + preventDefault: boolean; + stopPropagation: boolean; + stopImmediatePropagation: boolean; + filesRequested: boolean; +}; +// This function sends the event to the virtualdom and then waits for the virtualdom to process it +// +// However, it's not really suitable for liveview, because it's synchronous and will block the main thread +// We should definitely consider using a websocket if we want to block... or just not block on liveview +// Liveview is a little bit of a tricky beast +function handleVirtualdomEventSync(contents: string): EventSyncResult { // Handle the event on the virtualdom and then process whatever its output was const xhr = new XMLHttpRequest(); @@ -157,130 +247,29 @@ export function waitForVirtualDomPreventDefault(contents: string): boolean { xhr.send(contents); // Deserialize the response, and then prevent the default/capture the event if the virtualdom wants to - return JSON.parse(xhr.responseText).preventDefault; + return JSON.parse(xhr.responseText); } - -async function read_files(target: HTMLInputElement, contents, bubbles, realId, name) { - let files = target.files!; - let file_contents: { [name: string]: number[] } = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - - contents.files = { files: file_contents }; - - if (realId === null) { - return; - } - - const message = window.interpreter.serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }); - - window.ipc.postMessage(message); -} - - -export function target_id(target: EventTarget): string | null { - if (!(target instanceof Element)) { +function targetId(target: EventTarget): number | null { + // Ensure that the target is a node, sometimes it's nota + if (!(target instanceof Node)) { return null; } - function find_real_id(target: Element): string | null { - let realId = target.getAttribute(`data-dioxus-id`); + let ourTarget = target; + let realId = null; - // 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 null; - } - - target = target.parentElement; - if (target instanceof Element) { - realId = target.getAttribute(`data-dioxus-id`); - } + while (realId == null) { + if (ourTarget === null) { + return null; } - return realId; - } - - return find_real_id(target); -} - - - -// This should: -// - prevent form submissions from navigating -// - prevent anchor tags from navigating -// - prevent buttons from submitting forms -// - let the virtualdom attempt to prevent the event -preventDefaults(event: Event, target: EventTarget) { - let preventDefaultRequests: string | null = 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 (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - // Attempt to intercept if the event is a click - if (target instanceof Element && event.type === "click") { - this.preventFormNavigate(event, target); - } -} - -preventFormNavigate(event: Event, target: Element) { - // todo call prevent default if it's the right type of event - if (!this.intercept_link_redirects) { - return; - } - - // prevent buttons in forms from submitting the form - if (target.tagName === "BUTTON") { // && event.type == "submit" - event.preventDefault(); - } - - // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating - let a_element = target.closest("a"); - if (a_element == null) { - return; - } - - 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( - window.interpreter.serializeIpcMessage("browser_open", { href }) - ); + if (ourTarget instanceof Element) { + realId = ourTarget.getAttribute(`data-dioxus-id`); } + + ourTarget = ourTarget.parentNode; } + + return parseInt(realId); } diff --git a/packages/interpreter/src/ts/interpreter_web.ts b/packages/interpreter/src/ts/interpreter_web.ts index 6a16761e7..628a283f8 100644 --- a/packages/interpreter/src/ts/interpreter_web.ts +++ b/packages/interpreter/src/ts/interpreter_web.ts @@ -4,10 +4,12 @@ // // We're using sledgehammer directly -import { Interpreter, m } from "./interpreter_core"; +import { Interpreter } from "./interpreter_core"; export class WebInterpreter extends Interpreter { - constructor(root: Element, handler: EventListener) { + m: any; + + constructor(root: HTMLElement, handler: EventListener) { super(root, handler); } @@ -17,7 +19,7 @@ export class WebInterpreter extends Interpreter { let ptr_end = ptr + len; for (; ptr < ptr_end; ptr++) { - let end = m.getUint8(ptr); + let end = this.m.getUint8(ptr); for (node = node.firstChild; end > 0; end--) { node = node.nextSibling; } @@ -26,7 +28,7 @@ export class WebInterpreter extends Interpreter { return node; } - saveTemplate(nodes: Element[], tmpl_id: string) { + saveTemplate(nodes: HTMLElement[], tmpl_id: string) { this.templates[tmpl_id] = nodes; } @@ -34,7 +36,7 @@ export class WebInterpreter extends Interpreter { const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); for (let i = 0; i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i]; + const hydrateNode = hydrateNodes[i] as HTMLElement; const hydration = hydrateNode.getAttribute('data-node-hydration'); const split = hydration!.split(','); const id = ids[parseInt(split[0])]; @@ -54,22 +56,27 @@ export class WebInterpreter extends Interpreter { } } } + 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 as Element; + this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; } + currentNode = treeWalker.nextNode(); } } - getNode(id: number): Element { + getNode(id: number): Node { return this.nodes[id]; } @@ -77,7 +84,7 @@ export class WebInterpreter extends Interpreter { const root = this.nodes[id]; const els = this.stack.splice(this.stack.length - many); for (let k = 0; k < many; k++) { - this.root.appendChild(els[k]); + root.appendChild(els[k]); } } } diff --git a/packages/interpreter/src/ts/serialize.ts b/packages/interpreter/src/ts/serialize.ts index e7473155a..03f17434a 100644 --- a/packages/interpreter/src/ts/serialize.ts +++ b/packages/interpreter/src/ts/serialize.ts @@ -1,12 +1,12 @@ // Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {}; -type SerializedEvent = { - values?: { [key: string]: FormDataEntryValue[] } | FormDataEntryValue[]; +export type SerializedEvent = { + values?: { [key: string]: FormDataEntryValue[] }; value?: string; [key: string]: any; }; -function serializeEvent(event: Event): SerializedEvent { +export function serializeEvent(event: Event): SerializedEvent { // copy, cut, paste if (event instanceof KeyboardEvent) { diff --git a/packages/interpreter/src/ts/set_attribute.ts b/packages/interpreter/src/ts/set_attribute.ts index cd94f5507..5fcd3c91f 100644 --- a/packages/interpreter/src/ts/set_attribute.ts +++ b/packages/interpreter/src/ts/set_attribute.ts @@ -1,3 +1,5 @@ +// A unified interface for setting attributes on a node + // this function should try and stay fast, if possible export function setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { // we support a single namespace by default: style diff --git a/packages/interpreter/tsconfig.json b/packages/interpreter/tsconfig.json index 18ce8679c..0f5c66032 100644 --- a/packages/interpreter/tsconfig.json +++ b/packages/interpreter/tsconfig.json @@ -3,7 +3,10 @@ "module": "system", "lib": [ "ES2015", - "DOM" + "DOM", + "dom", + "dom.iterable", + "esnext" ], "noImplicitAny": true, "removeComments": true,