diff --git a/examples/nested_listeners.rs b/examples/nested_listeners.rs new file mode 100644 index 000000000..4b9fd14e0 --- /dev/null +++ b/examples/nested_listeners.rs @@ -0,0 +1,33 @@ +//! Nested Listeners +//! +//! This example showcases how to control event bubbling from child to parents. +//! +//! Both web and desktop support bubbling and bubble cancelation. + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + onclick: move |_| println!("clicked! top"), + button { + onclick: move |_| println!("clicked! bottom propoate"), + "Propogate" + } + button { + onclick: move |evt| { + println!("clicked! bottom no bubbling"); + evt.cancel_bubble(); + }, + "Dont propogate" + } + button { + "Does not handle clicks" + } + } + }) +} diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index c17a63d2a..88fcba12a 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -13,8 +13,10 @@ - - - + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 802e7441c..bc9a84e42 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -205,7 +205,7 @@ pub fn launch_with_props( } None }) - .with_custom_protocol("dioxus".into(), move |request| { + .with_custom_protocol(String::from("dioxus"), move |request| { // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case" // For now, we only serve two pieces of content which get included as bytes into the final binary. let path = request.uri().replace("dioxus://", ""); @@ -225,10 +225,10 @@ pub fn launch_with_props( } }) .with_file_drop_handler(move |window, evet| { - if let Some(handler) = file_handler.as_ref() { - return handler(window, evet); - } - false + file_handler + .as_ref() + .map(|handler| handler(window, evet)) + .unwrap_or_default() }); for (name, handler) in cfg.protocos.drain(..) { diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index 9da95fa7c..ea50ec3d3 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,569 +1,499 @@ +export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +export class Interpreter { + root; + stack; + listeners; + handlers; + lastNodeWasText; + nodes; + constructor(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + top() { + return this.stack[this.stack.length - 1]; + } + pop() { + return this.stack.pop(); + } + PushRoot(root) { + const node = this.nodes[root]; + this.stack.push(node); + } + AppendChildren(many) { + let root = this.stack[this.stack.length - (1 + many)]; + let to_add = this.stack.splice(this.stack.length - many); + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + } + ReplaceWith(root_id, m) { + let root = this.nodes[root_id]; + let els = this.stack.splice(this.stack.length - m); + root.replaceWith(...els); + } + InsertAfter(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + InsertBefore(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + Remove(root) { + let node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + } + CreateTextNode(text, root) { + // todo: make it so the types are okay + const node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + } + CreateElement(tag, root) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + this.nodes[root] = el; + this.stack.push(el); + } + CreateElementNs(tag, root, ns) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + CreatePlaceholder(root) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + NewEventListener(event_name, root, handler) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + else { + this.listeners[event_name]++; + } + } + RemoveEventListener(root, event_name) { + const element = this.nodes[root]; + element.removeAttribute(`data-dioxus-id`); + this.listeners[event_name]--; + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; + } + } + SetText(root, text) { + this.nodes[root].textContent = text; + } + SetAttribute(root, field, value, ns) { + const name = field; + const node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } + else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } + else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + 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 (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } + else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root, name) { + const node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + } + handleEdits(edits) { + this.stack.push(this.root); + for (let edit of edits) { + this.handleEdit(edit); + } + } + handleEdit(edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event) => { + let target = event.target; + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + let contents = serialize_event(event); + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + if (event.type == "submit") { + event.preventDefault(); + } + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + if (realId == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} function serialize_event(event) { - var _a, _b; - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; + 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, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location, + repeat: repeat, + which: which, + locale: "locale", + }; + } + 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, + }; + } + 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, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, 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, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + }; + } + 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 {}; + } } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - var data = event.data; - return { - data: data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - var _c = event, - charCode = _c.charCode, - key = _c.key, - altKey = _c.altKey, - ctrlKey = _c.ctrlKey, - metaKey = _c.metaKey, - keyCode = _c.keyCode, - shiftKey = _c.shiftKey, - location_1 = _c.location, - repeat = _c.repeat, - which = _c.which; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location_1, - repeat: repeat, - which: which, - locale: "locale", - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - var target = event.target; - var value = void 0; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = - (_a = target.value) !== null && _a !== void 0 - ? _a - : target.textContent; - } - return { - value: value, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - var target = event.target; - var value = - (_b = target.value) !== null && _b !== void 0 ? _b : target.textContent; - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - return { - value: value, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - var _d = event, - altKey = _d.altKey, - button = _d.button, - buttons = _d.buttons, - clientX = _d.clientX, - clientY = _d.clientY, - ctrlKey = _d.ctrlKey, - metaKey = _d.metaKey, - pageX = _d.pageX, - pageY = _d.pageY, - screenX_1 = _d.screenX, - screenY_1 = _d.screenY, - shiftKey = _d.shiftKey; - 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_1, - screen_y: screenY_1, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - var _e = event, - altKey = _e.altKey, - button = _e.button, - buttons = _e.buttons, - clientX = _e.clientX, - clientY = _e.clientY, - ctrlKey = _e.ctrlKey, - metaKey = _e.metaKey, - pageX = _e.pageX, - pageY = _e.pageY, - screenX_2 = _e.screenX, - screenY_2 = _e.screenY, - shiftKey = _e.shiftKey, - pointerId = _e.pointerId, - width = _e.width, - height = _e.height, - pressure = _e.pressure, - tangentialPressure = _e.tangentialPressure, - tiltX = _e.tiltX, - tiltY = _e.tiltY, - twist = _e.twist, - pointerType = _e.pointerType, - isPrimary = _e.isPrimary; - 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_2, - screen_y: screenY_2, - 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": { - var _f = event, - altKey = _f.altKey, - ctrlKey = _f.ctrlKey, - metaKey = _f.metaKey, - shiftKey = _f.shiftKey; - 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": { - var _g = event, - deltaX = _g.deltaX, - deltaY = _g.deltaY, - deltaZ = _g.deltaZ, - deltaMode = _g.deltaMode; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - var _h = event, - animationName = _h.animationName, - elapsedTime = _h.elapsedTime, - pseudoElement = _h.pseudoElement; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - var _j = event, - propertyName = _j.propertyName, - elapsedTime = _j.elapsedTime, - pseudoElement = _j.pseudoElement; - 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 {}; - } - } } -var 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, +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, }; -var Interpreter = /** @class */ (function () { - function Interpreter(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - Interpreter.prototype.top = function () { - return this.stack[this.stack.length - 1]; - }; - Interpreter.prototype.pop = function () { - return this.stack.pop(); - }; - Interpreter.prototype.PushRoot = function (root) { - var node = this.nodes[root]; - this.stack.push(node); - }; - Interpreter.prototype.AppendChildren = function (many) { - var root = this.stack[this.stack.length - (1 + many)]; - var to_add = this.stack.splice(this.stack.length - many); - for (var i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - }; - Interpreter.prototype.ReplaceWith = function (root_id, m) { - var root = this.nodes[root_id]; - var els = this.stack.splice(this.stack.length - m); - root.replaceWith.apply(root, els); - }; - Interpreter.prototype.InsertAfter = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.after.apply(old, new_nodes); - }; - Interpreter.prototype.InsertBefore = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.before.apply(old, new_nodes); - }; - Interpreter.prototype.Remove = function (root) { - var node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - }; - Interpreter.prototype.CreateTextNode = function (text, root) { - // todo: make it so the types are okay - var node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - }; - Interpreter.prototype.CreateElement = function (tag, root) { - var el = document.createElement(tag); - el.setAttribute("dioxus-id", "".concat(root)); - this.nodes[root] = el; - this.stack.push(el); - }; - Interpreter.prototype.CreateElementNs = function (tag, root, ns) { - var el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.CreatePlaceholder = function (root) { - var el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.NewEventListener = function ( - event_name, - scope, - root, - handler - ) { - // console.log('new event listener', event_name, root, scope); - var element = this.nodes[root]; - element.setAttribute( - "dioxus-event-".concat(event_name), - "".concat(scope, ".").concat(root) - ); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } else { - this.listeners[event_name]++; - } - }; - Interpreter.prototype.RemoveEventListener = function (root, event_name) { - var element = this.nodes[root]; - element.removeAttribute("dioxus-event-".concat(event_name)); - this.listeners[event_name]--; - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; - } - }; - Interpreter.prototype.SetText = function (root, text) { - this.nodes[root].textContent = text; - }; - Interpreter.prototype.SetAttribute = function (root, field, value, ns) { - var name = field; - var node = this.nodes[root]; - if (ns == "style") { - // @ts-ignore - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - 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 (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - }; - Interpreter.prototype.RemoveAttribute = function (root, name) { - var node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - }; - Interpreter.prototype.handleEdits = function (edits) { - this.stack.push(this.root); - for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { - var edit = edits_1[_i]; - this.handleEdit(edit); - } - }; - Interpreter.prototype.handleEdit = function (edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // todo: only on desktop should we make our own handler - var handler = function (event) { - var target = event.target; - // console.log("event", event); - if (target != null) { - var realId = target.getAttribute("dioxus-id"); - var shouldPreventDefault = target.getAttribute( - "dioxus-prevent-default" - ); - var contents = serialize_event(event); - if (shouldPreventDefault === "on".concat(event.type)) { - event.preventDefault(); - } - - if (event.type == "submit") { - event.preventDefault(); - } - - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - var href = target.getAttribute("href"); - if ( - href !== "" && - href !== null && - href !== undefined && - realId != null - ) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href: href, - }); - } - } - } - } - - if (realId == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - }; - return Interpreter; -})(); -export { Interpreter }; -function main() { - var root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} -//# sourceMappingURL=interpreter.js.map diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index 533536277..fdd8c671d 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -1,3 +1,319 @@ + + + + +export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} + +declare global { + interface Window { + interpreter: Interpreter; + rpc: { call: (method: string, args?: any) => void }; + } +} + + +export class Interpreter { + root: Element; + stack: Element[]; + listeners: { [key: string]: number }; + handlers: { [key: string]: (evt: Event) => void }; + lastNodeWasText: boolean; + nodes: Element[]; + + + constructor(root: Element) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + + top() { + return this.stack[this.stack.length - 1]; + } + + pop() { + return this.stack.pop(); + } + + PushRoot(root: number) { + const node = this.nodes[root]; + this.stack.push(node); + } + + AppendChildren(many: number) { + let root = this.stack[this.stack.length - (1 + many)]; + + let to_add = this.stack.splice(this.stack.length - many); + + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + } + + ReplaceWith(root_id: number, m: number) { + let root = this.nodes[root_id] as Element; + let els = this.stack.splice(this.stack.length - m); + + root.replaceWith(...els); + } + + InsertAfter(root: number, n: number) { + let old = this.nodes[root] as Element; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + + InsertBefore(root: number, n: number) { + let old = this.nodes[root] as Element; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + + Remove(root: number) { + let node = this.nodes[root] as Element; + if (node !== undefined) { + node.remove(); + } + } + + CreateTextNode(text: string, root: number) { + // todo: make it so the types are okay + const node = document.createTextNode(text) as any as Element; + this.nodes[root] = node; + this.stack.push(node); + } + + CreateElement(tag: string, root: number) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + + this.nodes[root] = el; + this.stack.push(el); + } + + CreateElementNs(tag: string, root: number, ns: string) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + + CreatePlaceholder(root: number) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + + NewEventListener(event_name: string, root: number, handler: (evt: Event) => void) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } else { + this.listeners[event_name]++; + } + } + + RemoveEventListener(root: number, event_name: string) { + const element = this.nodes[root]; + element.removeAttribute(`data-dioxus-id`); + + this.listeners[event_name]--; + + if (this.listeners[event_name] === 0) { + this.root.removeEventListener(event_name, this.handlers[event_name]); + delete this.listeners[event_name]; + delete this.handlers[event_name]; + } + } + + + SetText(root: number, text: string) { + this.nodes[root].textContent = text; + } + + SetAttribute(root: number, field: string, value: string, ns: string | undefined) { + const name = field; + const node = this.nodes[root]; + + if (ns == "style") { + + // @ts-ignore + (node as HTMLElement).style[name] = value; + + } else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value != (node as HTMLInputElement).value) { + (node as HTMLInputElement).value = value; + } + break; + case "checked": + (node as HTMLInputElement).checked = value === "true"; + break; + case "selected": + (node as HTMLOptionElement).selected = value === "true"; + 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 (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root: number, name: string) { + + const node = this.nodes[root]; + node.removeAttribute(name); + + if (name === "value") { + (node as HTMLInputElement).value = ""; + } + + if (name === "checked") { + (node as HTMLInputElement).checked = false; + } + + if (name === "selected") { + (node as HTMLOptionElement).selected = false; + } + } + + handleEdits(edits: DomEdit[]) { + this.stack.push(this.root); + + for (let edit of edits) { + this.handleEdit(edit); + } + } + + handleEdit(edit: DomEdit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + + + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event: Event) => { + let target = event.target as Element | null; + + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + + let contents = serialize_event(event); + + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + + if (event.type == "submit") { + event.preventDefault(); + } + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href") + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + + if (realId == null) { + return; + } + + window.rpc.call("user_event", { + event: (edit as NewEventListener).event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} + + + function serialize_event(event: Event) { switch (event.type) { case "copy": @@ -327,307 +643,6 @@ const bool_attrs = { truespeed: true, }; -export class Interpreter { - root: Element; - stack: Element[]; - listeners: { [key: string]: number }; - handlers: { [key: string]: (evt: Event) => void }; - lastNodeWasText: boolean; - nodes: Element[]; - - - constructor(root: Element) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - - top() { - return this.stack[this.stack.length - 1]; - } - - pop() { - return this.stack.pop(); - } - - PushRoot(root: number) { - const node = this.nodes[root]; - this.stack.push(node); - } - - AppendChildren(many: number) { - let root = this.stack[this.stack.length - (1 + many)]; - - let to_add = this.stack.splice(this.stack.length - many); - - for (let i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - } - - ReplaceWith(root_id: number, m: number) { - let root = this.nodes[root_id] as Element; - let els = this.stack.splice(this.stack.length - m); - - root.replaceWith(...els); - } - - InsertAfter(root: number, n: number) { - let old = this.nodes[root] as Element; - let new_nodes = this.stack.splice(this.stack.length - n); - old.after(...new_nodes); - } - - InsertBefore(root: number, n: number) { - let old = this.nodes[root] as Element; - let new_nodes = this.stack.splice(this.stack.length - n); - old.before(...new_nodes); - } - - Remove(root: number) { - let node = this.nodes[root] as Element; - if (node !== undefined) { - node.remove(); - } - } - - CreateTextNode(text: string, root: number) { - // todo: make it so the types are okay - const node = document.createTextNode(text) as any as Element; - this.nodes[root] = node; - this.stack.push(node); - } - - CreateElement(tag: string, root: number) { - const el = document.createElement(tag); - el.setAttribute("dioxus-id", `${root}`); - - this.nodes[root] = el; - this.stack.push(el); - } - - CreateElementNs(tag: string, root: number, ns: string) { - let el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - } - - CreatePlaceholder(root: number) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - - NewEventListener(event_name: string, scope: number, root: number, handler: (evt: Event) => void) { - // console.log('new event listener', event_name, root, scope); - const element = this.nodes[root]; - element.setAttribute( - `dioxus-event-${event_name}`, - `${scope}.${root}` - ); - - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } else { - this.listeners[event_name]++; - } - } - - RemoveEventListener(root: number, event_name: string) { - const element = this.nodes[root]; - element.removeAttribute( - `dioxus-event-${event_name}` - ); - - this.listeners[event_name]--; - - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; - } - } - - - SetText(root: number, text: string) { - this.nodes[root].textContent = text; - } - - SetAttribute(root: number, field: string, value: string, ns: string | undefined) { - const name = field; - const node = this.nodes[root]; - - if (ns == "style") { - - // @ts-ignore - (node as HTMLElement).style[name] = value; - - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value != (node as HTMLInputElement).value) { - (node as HTMLInputElement).value = value; - } - break; - case "checked": - (node as HTMLInputElement).checked = value === "true"; - break; - case "selected": - (node as HTMLOptionElement).selected = value === "true"; - 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 (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root: number, name: string) { - - const node = this.nodes[root]; - node.removeAttribute(name); - - if (name === "value") { - (node as HTMLInputElement).value = ""; - } - - if (name === "checked") { - (node as HTMLInputElement).checked = false; - } - - if (name === "selected") { - (node as HTMLOptionElement).selected = false; - } - } - - handleEdits(edits: DomEdit[]) { - this.stack.push(this.root); - - for (let edit of edits) { - this.handleEdit(edit); - } - } - - handleEdit(edit: DomEdit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - - - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event: Event) => { - const target = event.target as Element | null; - - if (target != null) { - const realId = target.getAttribute(`dioxus-id`); - - const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - - let contents = serialize_event(event); - - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - - if (event.type == "submit") { - event.preventDefault(); - } - - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href") - if (href !== "" && href !== null && href !== undefined && realId != null) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href - }); - } - } - } - } - - if (realId == null) { - return; - } - - window.rpc.call("user_event", { - event: (edit as NewEventListener).event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - } -} - -function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} type PushRoot = { type: "PushRoot", root: number }; @@ -663,14 +678,3 @@ type DomEdit = SetText | SetAttribute | RemoveAttribute; - - -export { }; -declare global { - interface Window { - interpreter: Interpreter; - rpc: { call: (method: string, args?: any) => void }; - } -} - - diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json index 883760881..f63034044 100644 --- a/packages/jsinterpreter/tsconfig.json +++ b/packages/jsinterpreter/tsconfig.json @@ -1,13 +1,13 @@ { "compilerOptions": { - "target": "ES5", - "module": "ESNext", + "target": "ESNext", + "module": "ES2015", "lib": [ "es2015", "es5", "es6", "dom" ], - "strict": true + "strict": true, } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index b0b4dd035..ffbef9f4b 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -277,9 +277,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc anyhow::Result { - use anyhow::Context; - - let target = event + let mut target = event .target() .expect("missing target") .dyn_into::() @@ -287,18 +285,36 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { let typ = event.type_(); - let element_id = target - .get_attribute("dioxus-id") - .context("Could not find element id on event target")? - .parse()?; - - Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: Some(ElementId(element_id)), - scope_id: None, - priority: dioxus_core::EventPriority::Medium, - }) + loop { + match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { + Some(Ok(id)) => { + return Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: Some(ElementId(id)), + scope_id: None, + priority: dioxus_core::EventPriority::Medium, + }); + } + Some(Err(e)) => { + return Err(e.into()); + } + None => { + // walk the tree upwards until we actually find an event target + if let Some(parent) = target.parent_element() { + target = parent; + } else { + return Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: None, + scope_id: None, + priority: dioxus_core::EventPriority::Low, + }); + } + } + } + } } pub(crate) fn load_document() -> Document {