From 5bf6c96f9fed04de949403202bafcbeadb5d2030 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 12 Jan 2022 07:34:37 -0500 Subject: [PATCH 1/4] feat: setup a typescript build --- packages/desktop/src/index.html | 5 +- packages/desktop/src/index.js | 446 ------------------ packages/desktop/src/lib.rs | 2 +- packages/jsinterpreter/interpreter.js | 461 +++++++++++++++++++ packages/jsinterpreter/interpreter.ts | 636 ++++++++++++++++++++++++++ packages/jsinterpreter/tsconfig.json | 10 + 6 files changed, 1111 insertions(+), 449 deletions(-) delete mode 100644 packages/desktop/src/index.js create mode 100644 packages/jsinterpreter/interpreter.js create mode 100644 packages/jsinterpreter/interpreter.ts create mode 100644 packages/jsinterpreter/tsconfig.json diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index a67432e98..888ab8437 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -12,7 +12,8 @@
- + + + diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js deleted file mode 100644 index 88cf9fcdc..000000000 --- a/packages/desktop/src/index.js +++ /dev/null @@ -1,446 +0,0 @@ -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, -}; - -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": - return {}; - - case "compositionend": - case "compositionstart": - case "compositionupdate": - return { - data: event.data, - }; - - case "keydown": - case "keypress": - case "keyup": - return { - char_code: event.charCode, - key: event.key, - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - key_code: event.keyCode, - shift_key: event.shiftKey, - locale: "locale", - location: event.location, - repeat: event.repeat, - which: event.which, - // locale: event.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": - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - }; - - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - pointer_id: event.pointerId, - width: event.width, - height: event.height, - pressure: event.pressure, - tangential_pressure: event.tangentialPressure, - tilt_x: event.tiltX, - tilt_y: event.tiltY, - twist: event.twist, - pointer_type: event.pointerType, - is_primary: event.isPrimary, - }; - - case "select": - return {}; - - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": - return { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - }; - - case "scroll": - return {}; - - case "wheel": - return { - delta_x: event.deltaX, - delta_y: event.deltaY, - delta_z: event.deltaZ, - delta_mode: event.deltaMode, - }; - - case "animationstart": - case "animationend": - case "animationiteration": - return { - animation_name: event.animationName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - }; - - case "transitionend": - return { - property_name: event.propertyName, - elapsed_time: event.elapsedTime, - pseudo_element: event.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 {}; - } -} - -class Interpreter { - constructor(root) { - this.root = root; - this.stack = [root]; - this.listeners = { - onclick: {}, - }; - this.lastNodeWasText = false; - this.nodes = [root]; - } - - top() { - return this.stack[this.stack.length - 1]; - } - - pop() { - return this.stack.pop(); - } - - PushRoot(edit) { - const id = edit.root; - const node = this.nodes[id]; - this.stack.push(node); - } - - AppendChildren(edit) { - let root = this.stack[this.stack.length - (1 + edit.many)]; - - let to_add = this.stack.splice(this.stack.length - edit.many); - - for (let i = 0; i < edit.many; i++) { - root.appendChild(to_add[i]); - } - } - - ReplaceWith(edit) { - let root = this.nodes[edit.root]; - let els = this.stack.splice(this.stack.length - edit.m); - - root.replaceWith(...els); - } - - InsertAfter(edit) { - let old = this.nodes[edit.root]; - let new_nodes = this.stack.splice(this.stack.length - edit.n); - old.after(...new_nodes); - } - - InsertBefore(edit) { - let old = this.nodes[edit.root]; - let new_nodes = this.stack.splice(this.stack.length - edit.n); - old.before(...new_nodes); - } - - Remove(edit) { - let node = this.nodes[edit.root]; - if (node !== undefined) { - node.remove(); - } - } - - CreateTextNode(edit) { - const node = document.createTextNode(edit.text); - this.nodes[edit.root] = node; - this.stack.push(node); - } - - CreateElement(edit) { - const tagName = edit.tag; - const el = document.createElement(tagName); - this.nodes[edit.root] = el; - el.setAttribute("dioxus-id", edit.root); - this.stack.push(el); - } - - CreateElementNs(edit) { - let el = document.createElementNS(edit.ns, edit.tag); - this.stack.push(el); - this.nodes[edit.root] = el; - } - - CreatePlaceholder(edit) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[edit.root] = el; - } - - RemoveEventListener(edit) {} - - NewEventListener(edit) { - const event_name = edit.event_name; - const mounted_node_id = edit.root; - const scope = edit.scope; - - const element = this.nodes[edit.root]; - element.setAttribute( - `dioxus-event-${event_name}`, - `${scope}.${mounted_node_id}` - ); - - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = true; - - this.root.addEventListener(event_name, (event) => { - const target = event.target; - const real_id = target.getAttribute(`dioxus-id`); - - const should_prevent_default = target.getAttribute( - `dioxus-prevent-default` - ); - - let contents = serialize_event(event); - - if (should_prevent_default === `on${event.type}`) { - event.preventDefault(); - } - - if (real_id == null) { - return; - } - - rpc.call("user_event", { - event: event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); - }); - } - } - - SetText(edit) { - this.nodes[edit.root].textContent = edit.text; - } - - SetAttribute(edit) { - // console.log("setting attr", edit); - const name = edit.field; - const value = edit.value; - const ns = edit.ns; - const node = this.nodes[edit.root]; - - if (ns == "style") { - 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(edit) { - const name = edit.field; - const node = this.nodes[edit.root]; - node.removeAttribute(name); - - if (name === "value") { - node.value = null; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - } - - handleEdits(edits) { - this.stack.push(this.root); - - for (let x = 0; x < edits.length; x++) { - let edit = edits[x]; - let f = this[edit.type]; - f.call(this, edit); - } - } -} - -function main() { - let root = window.document.getElementById("main"); - window.interpreter = new Interpreter(root); - rpc.call("initialize"); -} - -main(); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0b6044761..02e1dca82 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -203,7 +203,7 @@ pub fn launch_with_props( } else if path.trim_end_matches('/') == "index.html/index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") - .body(include_bytes!("./index.js").to_vec()) + .body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec()) } else { wry::http::ResponseBuilder::new() .status(wry::http::status::StatusCode::NOT_FOUND) diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js new file mode 100644 index 000000000..ac2e7adb5 --- /dev/null +++ b/packages/jsinterpreter/interpreter.js @@ -0,0 +1,461 @@ +"use strict"; +exports.__esModule = true; +function serialize_event(event) { + var _a, _b; + switch (event.type) { + case "copy": + case "cut": + case "past": + 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_1 = event.target; + var value_1 = (_b = target_1.value) !== null && _b !== void 0 ? _b : target_1.textContent; + if (target_1.type == "checkbox") { + value_1 = target_1.checked ? "true" : "false"; + } + return { + value: value_1 + }; + } + 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_1 = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey_1 = _d.ctrlKey, metaKey_1 = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey_1 = _d.shiftKey; + return { + alt_key: altKey_1, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_1, + meta_key: metaKey_1, + page_x: pageX, + page_y: pageY, + screen_x: screenX_1, + screen_y: screenY_1, + shift_key: shiftKey_1 + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + var _e = event, altKey_2 = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey_2 = _e.ctrlKey, metaKey_2 = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey_2 = _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_2, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_2, + meta_key: metaKey_2, + page_x: pageX, + page_y: pageY, + screen_x: screenX_2, + screen_y: screenY_2, + shift_key: shiftKey_2, + 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_3 = _f.altKey, ctrlKey_3 = _f.ctrlKey, metaKey_3 = _f.metaKey, shiftKey_3 = _f.shiftKey; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey_3, + ctrl_key: ctrlKey_3, + meta_key: metaKey_3, + shift_key: shiftKey_3 + }; + } + 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 +}; +var Interpreter = /** @class */ (function () { + function Interpreter(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + 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 (edit) { + var id = edit.root; + var node = this.nodes[id]; + this.stack.push(node); + }; + Interpreter.prototype.AppendChildren = function (edit) { + var root = this.stack[this.stack.length - (1 + edit.many)]; + var to_add = this.stack.splice(this.stack.length - edit.many); + for (var i = 0; i < edit.many; i++) { + root.appendChild(to_add[i]); + } + }; + Interpreter.prototype.ReplaceWith = function (edit) { + var root = this.nodes[edit.root]; + var els = this.stack.splice(this.stack.length - edit.m); + root.replaceWith.apply(root, els); + }; + Interpreter.prototype.InsertAfter = function (edit) { + var old = this.nodes[edit.root]; + var new_nodes = this.stack.splice(this.stack.length - edit.n); + old.after.apply(old, new_nodes); + }; + Interpreter.prototype.InsertBefore = function (edit) { + var old = this.nodes[edit.root]; + var new_nodes = this.stack.splice(this.stack.length - edit.n); + old.before.apply(old, new_nodes); + }; + Interpreter.prototype.Remove = function (edit) { + var node = this.nodes[edit.root]; + if (node !== undefined) { + node.remove(); + } + }; + Interpreter.prototype.CreateTextNode = function (edit) { + // todo: make it so the types are okay + var node = document.createTextNode(edit.text); + this.nodes[edit.root] = node; + this.stack.push(node); + }; + Interpreter.prototype.CreateElement = function (edit) { + var el = document.createElement(edit.tag); + el.setAttribute("dioxus-id", "".concat(edit.root)); + this.nodes[edit.root] = el; + this.stack.push(el); + }; + Interpreter.prototype.CreateElementNs = function (edit) { + var el = document.createElementNS(edit.ns, edit.tag); + this.stack.push(el); + this.nodes[edit.root] = el; + }; + Interpreter.prototype.CreatePlaceholder = function (edit) { + var el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[edit.root] = el; + }; + Interpreter.prototype.RemoveEventListener = function (edit) { }; + Interpreter.prototype.NewEventListener = function (edit, handler) { + var event_name = edit.event_name; + var mounted_node_id = edit.root; + var scope = edit.scope; + console.log('new event listener', event_name, mounted_node_id, scope); + var element = this.nodes[edit.root]; + element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(mounted_node_id)); + if (!this.listeners[event_name]) { + this.listeners[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + }; + Interpreter.prototype.SetText = function (edit) { + this.nodes[edit.root].textContent = edit.text; + }; + Interpreter.prototype.SetAttribute = function (edit) { + // console.log("setting attr", edit); + var name = edit.field; + var value = edit.value; + var ns = edit.ns; + var node = this.nodes[edit.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 (edit) { + var name = edit.name; + var node = this.nodes[edit.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) { + console.log("handling edits ", edits); + this.stack.push(this.root); + var _loop_1 = function (edit) { + switch (edit.type) { + case "AppendChildren": + this_1.AppendChildren(edit); + break; + case "ReplaceWith": + this_1.ReplaceWith(edit); + break; + case "InsertAfter": + this_1.InsertAfter(edit); + break; + case "InsertBefore": + this_1.InsertBefore(edit); + break; + case "Remove": + this_1.Remove(edit); + break; + case "CreateTextNode": + this_1.CreateTextNode(edit); + break; + case "CreateElement": + this_1.CreateElement(edit); + break; + case "CreateElementNs": + this_1.CreateElementNs(edit); + break; + case "CreatePlaceholder": + this_1.CreatePlaceholder(edit); + break; + case "RemoveEventListener": + this_1.RemoveEventListener(edit); + 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 real_id = target.getAttribute("dioxus-id"); + var should_prevent_default = target.getAttribute("dioxus-prevent-default"); + var contents = serialize_event(event); + if (should_prevent_default === "on".concat(event.type)) { + event.preventDefault(); + } + if (real_id == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(real_id), + contents: contents + }); + } + }; + this_1.NewEventListener(edit, handler); + break; + case "SetText": + this_1.SetText(edit); + break; + case "SetAttribute": + this_1.SetAttribute(edit); + break; + case "RemoveAttribute": + this_1.RemoveAttribute(edit); + break; + } + }; + var this_1 = this; + for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { + var edit = edits_1[_i]; + _loop_1(edit); + } + }; + return Interpreter; +}()); +function main() { + var root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +main(); diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts new file mode 100644 index 000000000..faf9e1dab --- /dev/null +++ b/packages/jsinterpreter/interpreter.ts @@ -0,0 +1,636 @@ + + +function serialize_event(event: Event) { + switch (event.type) { + case "copy": + case "cut": + case "past": + return {}; + + case "compositionend": + case "compositionstart": + case "compositionupdate": + let { data } = (event as CompositionEvent); + return { + data, + }; + + case "keydown": + case "keypress": + case "keyup": + let { + charCode, + key, + altKey, + ctrlKey, + metaKey, + keyCode, + shiftKey, + location, + repeat, + which, + } = (event as KeyboardEvent); + + 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 as HTMLInputElement; + 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 as HTMLFormElement; + 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 as MouseEvent; + + 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 as PointerEvent; + 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 as TouchEvent; + 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 as WheelEvent; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + + case "animationstart": + case "animationend": + case "animationiteration": { + const { + animationName, + elapsedTime, + pseudoElement, + } = event as AnimationEvent; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + + case "transitionend": { + const { + propertyName, + elapsedTime, + pseudoElement, + } = event as TransitionEvent; + 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 {}; + } +} + +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, +}; + +class Interpreter { + root: Element; + stack: Element[]; + listeners: { [key: string]: (event: Event) => void }; + lastNodeWasText: boolean; + nodes: Element[]; + + + constructor(root: Element) { + this.root = root; + this.stack = [root]; + this.listeners = { + }; + this.lastNodeWasText = false; + this.nodes = [root]; + } + + top() { + return this.stack[this.stack.length - 1]; + } + + pop() { + return this.stack.pop(); + } + + PushRoot(edit: PushRoot) { + const id = edit.root; + const node = this.nodes[id]; + this.stack.push(node); + } + + AppendChildren(edit: AppendChildren) { + let root = this.stack[this.stack.length - (1 + edit.many)]; + + let to_add = this.stack.splice(this.stack.length - edit.many); + + for (let i = 0; i < edit.many; i++) { + root.appendChild(to_add[i]); + } + } + + ReplaceWith(edit: ReplaceWith) { + let root = this.nodes[edit.root] as Element; + let els = this.stack.splice(this.stack.length - edit.m); + + root.replaceWith(...els); + } + + InsertAfter(edit: InsertAfter) { + let old = this.nodes[edit.root] as Element; + let new_nodes = this.stack.splice(this.stack.length - edit.n); + old.after(...new_nodes); + } + + InsertBefore(edit: InsertBefore) { + let old = this.nodes[edit.root] as Element; + let new_nodes = this.stack.splice(this.stack.length - edit.n); + old.before(...new_nodes); + } + + Remove(edit: Remove) { + let node = this.nodes[edit.root] as Element; + if (node !== undefined) { + node.remove(); + } + } + + CreateTextNode(edit: CreateTextNode) { + // todo: make it so the types are okay + const node = document.createTextNode(edit.text) as any as Element; + this.nodes[edit.root] = node; + this.stack.push(node); + } + + CreateElement(edit: CreateElement) { + const el = document.createElement(edit.tag); + el.setAttribute("dioxus-id", `${edit.root}`); + + this.nodes[edit.root] = el; + this.stack.push(el); + } + + CreateElementNs(edit: CreateElementNs) { + let el = document.createElementNS(edit.ns, edit.tag); + this.stack.push(el); + this.nodes[edit.root] = el; + } + + CreatePlaceholder(edit: CreatePlaceholder) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[edit.root] = el; + } + + RemoveEventListener(edit: RemoveEventListener) { } + + NewEventListener(edit: NewEventListener, handler: (event: Event) => void) { + const event_name = edit.event_name; + const mounted_node_id = edit.root; + const scope = edit.scope; + console.log('new event listener', event_name, mounted_node_id, scope); + + const element = this.nodes[edit.root]; + element.setAttribute( + `dioxus-event-${event_name}`, + `${scope}.${mounted_node_id}` + ); + + if (!this.listeners[event_name]) { + this.listeners[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + } + + SetText(edit: SetText) { + this.nodes[edit.root].textContent = edit.text; + } + + SetAttribute(edit: SetAttribute) { + // console.log("setting attr", edit); + const name = edit.field; + const value = edit.value; + const ns = edit.ns; + const node = this.nodes[edit.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(edit: RemoveAttribute) { + const name = edit.name; + + const node = this.nodes[edit.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[]) { + console.log("handling edits ", edits); + this.stack.push(this.root); + + for (let edit of edits) { + switch (edit.type) { + case "AppendChildren": + this.AppendChildren(edit); + break; + case "ReplaceWith": + this.ReplaceWith(edit); + break; + case "InsertAfter": + this.InsertAfter(edit); + break; + case "InsertBefore": + this.InsertBefore(edit); + break; + case "Remove": + this.Remove(edit); + break; + case "CreateTextNode": + this.CreateTextNode(edit); + break; + case "CreateElement": + this.CreateElement(edit); + break; + case "CreateElementNs": + this.CreateElementNs(edit); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + let handler = (event: Event) => { + const target = event.target as Element | null; + console.log("event", event); + if (target != null) { + + const real_id = target.getAttribute(`dioxus-id`); + + const should_prevent_default = target.getAttribute( + `dioxus-prevent-default` + ); + + let contents = serialize_event(event); + + if (should_prevent_default === `on${event.type}`) { + event.preventDefault(); + } + + if (real_id == null) { + return; + } + + window.rpc.call("user_event", { + event: (edit as NewEventListener).event_name, + mounted_dom_id: parseInt(real_id), + contents: contents, + }); + } + }; + this.NewEventListener(edit, handler); + break; + case "SetText": + this.SetText(edit); + break; + case "SetAttribute": + this.SetAttribute(edit); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit); + 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 }; +type AppendChildren = { type: "AppendChildren", many: number }; +type ReplaceWith = { type: "ReplaceWith", root: number, m: number }; +type InsertAfter = { type: "InsertAfter", root: number, n: number }; +type InsertBefore = { type: "InsertBefore", root: number, n: number }; +type Remove = { type: "Remove", root: number }; +type CreateTextNode = { type: "CreateTextNode", text: string, root: number }; +type CreateElement = { type: "CreateElement", tag: string, root: number }; +type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string }; +type CreatePlaceholder = { type: "CreatePlaceholder", root: number }; +type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number }; +type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number }; +type SetText = { type: "SetText", root: number, text: string }; +type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined }; +type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string }; + + +type DomEdit = + PushRoot | + AppendChildren | + ReplaceWith | + InsertAfter | + InsertBefore | + Remove | + CreateTextNode | + CreateElement | + CreateElementNs | + CreatePlaceholder | + NewEventListener | + RemoveEventListener | + SetText | + SetAttribute | + RemoveAttribute; + + +export { }; +declare global { + interface Window { + interpreter: Interpreter; + rpc: { call: (method: string, args?: any) => void }; + } +} + + +type Edits = DomEdit[]; + +main(); diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json new file mode 100644 index 000000000..698781908 --- /dev/null +++ b/packages/jsinterpreter/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es5", + // "module": "None", + "module": "commonjs", + "strict": true, + "outDir": "dist", + "sourceMap": true + } +} From 10db6ad65bd23dabf60d0afbe6ff82cda8220a8b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 12 Jan 2022 08:57:42 -0500 Subject: [PATCH 2/4] wip: web now links against the js interprter code --- packages/jsinterpreter/interpreter.js | 243 ++++---- packages/jsinterpreter/interpreter.ts | 251 ++++---- packages/jsinterpreter/tsconfig.json | 1 - packages/web/Cargo.toml | 4 +- packages/web/src/bindings.rs | 59 ++ packages/web/src/dom.rs | 419 +------------- packages/web/src/interpreter.js | 529 +++++++++++++++++ packages/web/src/lib.rs | 1 + packages/web/src/olddom.rs | 789 ++++++++++++++++++++++++++ packages/web/src/rehydrate.rs | 10 +- 10 files changed, 1661 insertions(+), 645 deletions(-) create mode 100644 packages/web/src/bindings.rs create mode 100644 packages/web/src/interpreter.js create mode 100644 packages/web/src/olddom.rs diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index ac2e7adb5..6139280f8 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,5 +1,6 @@ "use strict"; exports.__esModule = true; +exports.Interpreter = void 0; function serialize_event(event) { var _a, _b; switch (event.type) { @@ -247,84 +248,79 @@ var Interpreter = /** @class */ (function () { Interpreter.prototype.pop = function () { return this.stack.pop(); }; - Interpreter.prototype.PushRoot = function (edit) { - var id = edit.root; - var node = this.nodes[id]; + Interpreter.prototype.PushRoot = function (root) { + var node = this.nodes[root]; this.stack.push(node); }; - Interpreter.prototype.AppendChildren = function (edit) { - var root = this.stack[this.stack.length - (1 + edit.many)]; - var to_add = this.stack.splice(this.stack.length - edit.many); - for (var i = 0; i < edit.many; i++) { + 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 (edit) { - var root = this.nodes[edit.root]; - var els = this.stack.splice(this.stack.length - edit.m); + 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 (edit) { - var old = this.nodes[edit.root]; - var new_nodes = this.stack.splice(this.stack.length - edit.n); + 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 (edit) { - var old = this.nodes[edit.root]; - var new_nodes = this.stack.splice(this.stack.length - edit.n); + 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 (edit) { - var node = this.nodes[edit.root]; + Interpreter.prototype.Remove = function (root) { + var node = this.nodes[root]; if (node !== undefined) { node.remove(); } }; - Interpreter.prototype.CreateTextNode = function (edit) { + Interpreter.prototype.CreateTextNode = function (text, root) { // todo: make it so the types are okay - var node = document.createTextNode(edit.text); - this.nodes[edit.root] = node; + var node = document.createTextNode(text); + this.nodes[root] = node; this.stack.push(node); }; - Interpreter.prototype.CreateElement = function (edit) { - var el = document.createElement(edit.tag); - el.setAttribute("dioxus-id", "".concat(edit.root)); - this.nodes[edit.root] = el; + 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 (edit) { - var el = document.createElementNS(edit.ns, edit.tag); + Interpreter.prototype.CreateElementNs = function (tag, root, ns) { + var el = document.createElementNS(ns, tag); this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; }; - Interpreter.prototype.CreatePlaceholder = function (edit) { + Interpreter.prototype.CreatePlaceholder = function (root) { var el = document.createElement("pre"); el.hidden = true; this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; }; - Interpreter.prototype.RemoveEventListener = function (edit) { }; - Interpreter.prototype.NewEventListener = function (edit, handler) { - var event_name = edit.event_name; - var mounted_node_id = edit.root; - var scope = edit.scope; - console.log('new event listener', event_name, mounted_node_id, scope); - var element = this.nodes[edit.root]; - element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(mounted_node_id)); - if (!this.listeners[event_name]) { - this.listeners[event_name] = handler; - this.root.addEventListener(event_name, handler); - } + Interpreter.prototype.NewEventListener = function (event_name, scope, root) { + 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]) { + // this.listeners[event_name] = handler; + // this.root.addEventListener(event_name, handler); + // } }; - Interpreter.prototype.SetText = function (edit) { - this.nodes[edit.root].textContent = edit.text; + Interpreter.prototype.RemoveEventListener = function (root, event_name, scope) { + // }; - Interpreter.prototype.SetAttribute = function (edit) { - // console.log("setting attr", edit); - var name = edit.field; - var value = edit.value; - var ns = edit.ns; - var node = this.nodes[edit.root]; + 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; @@ -359,9 +355,8 @@ var Interpreter = /** @class */ (function () { } } }; - Interpreter.prototype.RemoveAttribute = function (edit) { - var name = edit.name; - var node = this.nodes[edit.root]; + Interpreter.prototype.RemoveAttribute = function (root, name) { + var node = this.nodes[root]; node.removeAttribute(name); if (name === "value") { node.value = ""; @@ -376,81 +371,85 @@ var Interpreter = /** @class */ (function () { Interpreter.prototype.handleEdits = function (edits) { console.log("handling edits ", edits); this.stack.push(this.root); - var _loop_1 = function (edit) { - switch (edit.type) { - case "AppendChildren": - this_1.AppendChildren(edit); - break; - case "ReplaceWith": - this_1.ReplaceWith(edit); - break; - case "InsertAfter": - this_1.InsertAfter(edit); - break; - case "InsertBefore": - this_1.InsertBefore(edit); - break; - case "Remove": - this_1.Remove(edit); - break; - case "CreateTextNode": - this_1.CreateTextNode(edit); - break; - case "CreateElement": - this_1.CreateElement(edit); - break; - case "CreateElementNs": - this_1.CreateElementNs(edit); - break; - case "CreatePlaceholder": - this_1.CreatePlaceholder(edit); - break; - case "RemoveEventListener": - this_1.RemoveEventListener(edit); - 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 real_id = target.getAttribute("dioxus-id"); - var should_prevent_default = target.getAttribute("dioxus-prevent-default"); - var contents = serialize_event(event); - if (should_prevent_default === "on".concat(event.type)) { - event.preventDefault(); - } - if (real_id == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(real_id), - contents: contents - }); - } - }; - this_1.NewEventListener(edit, handler); - break; - case "SetText": - this_1.SetText(edit); - break; - case "SetAttribute": - this_1.SetAttribute(edit); - break; - case "RemoveAttribute": - this_1.RemoveAttribute(edit); - break; - } - }; - var this_1 = this; for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { var edit = edits_1[_i]; - _loop_1(edit); + 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, edit.scope); + 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 real_id = target.getAttribute("dioxus-id"); + var should_prevent_default = target.getAttribute("dioxus-prevent-default"); + var contents = serialize_event(event); + if (should_prevent_default === "on".concat(event.type)) { + event.preventDefault(); + } + if (real_id == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(real_id), + contents: contents + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root); + // this.NewEventListener(edit, 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; }()); +exports.Interpreter = Interpreter; function main() { var root = window.document.getElementById("main"); if (root != null) { diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index faf9e1dab..e3547effb 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -318,7 +318,7 @@ const bool_attrs = { truespeed: true, }; -class Interpreter { +export class Interpreter { root: Element; stack: Element[]; listeners: { [key: string]: (event: Event) => void }; @@ -343,106 +343,102 @@ class Interpreter { return this.stack.pop(); } - PushRoot(edit: PushRoot) { - const id = edit.root; - const node = this.nodes[id]; + PushRoot(root: number) { + const node = this.nodes[root]; this.stack.push(node); } - AppendChildren(edit: AppendChildren) { - let root = this.stack[this.stack.length - (1 + edit.many)]; + AppendChildren(many: number) { + let root = this.stack[this.stack.length - (1 + many)]; - let to_add = this.stack.splice(this.stack.length - edit.many); + let to_add = this.stack.splice(this.stack.length - many); - for (let i = 0; i < edit.many; i++) { + for (let i = 0; i < many; i++) { root.appendChild(to_add[i]); } } - ReplaceWith(edit: ReplaceWith) { - let root = this.nodes[edit.root] as Element; - let els = this.stack.splice(this.stack.length - edit.m); + 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(edit: InsertAfter) { - let old = this.nodes[edit.root] as Element; - let new_nodes = this.stack.splice(this.stack.length - edit.n); + 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(edit: InsertBefore) { - let old = this.nodes[edit.root] as Element; - let new_nodes = this.stack.splice(this.stack.length - edit.n); + 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(edit: Remove) { - let node = this.nodes[edit.root] as Element; + Remove(root: number) { + let node = this.nodes[root] as Element; if (node !== undefined) { node.remove(); } } - CreateTextNode(edit: CreateTextNode) { + CreateTextNode(text: string, root: number) { // todo: make it so the types are okay - const node = document.createTextNode(edit.text) as any as Element; - this.nodes[edit.root] = node; + const node = document.createTextNode(text) as any as Element; + this.nodes[root] = node; this.stack.push(node); } - CreateElement(edit: CreateElement) { - const el = document.createElement(edit.tag); - el.setAttribute("dioxus-id", `${edit.root}`); + CreateElement(tag: string, root: number) { + const el = document.createElement(tag); + el.setAttribute("dioxus-id", `${root}`); - this.nodes[edit.root] = el; + this.nodes[root] = el; this.stack.push(el); } - CreateElementNs(edit: CreateElementNs) { - let el = document.createElementNS(edit.ns, edit.tag); + CreateElementNs(tag: string, root: number, ns: string) { + let el = document.createElementNS(ns, tag); this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; } - CreatePlaceholder(edit: CreatePlaceholder) { + CreatePlaceholder(root: number) { let el = document.createElement("pre"); el.hidden = true; this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; } - RemoveEventListener(edit: RemoveEventListener) { } - - NewEventListener(edit: NewEventListener, handler: (event: Event) => void) { - const event_name = edit.event_name; - const mounted_node_id = edit.root; - const scope = edit.scope; - console.log('new event listener', event_name, mounted_node_id, scope); - - const element = this.nodes[edit.root]; + NewEventListener(event_name: string, scope: number, root: number) { + console.log('new event listener', event_name, root, scope); + const element = this.nodes[root]; element.setAttribute( `dioxus-event-${event_name}`, - `${scope}.${mounted_node_id}` + `${scope}.${root}` ); - if (!this.listeners[event_name]) { - this.listeners[event_name] = handler; - this.root.addEventListener(event_name, handler); - } + // if (!this.listeners[event_name]) { + // this.listeners[event_name] = handler; + // this.root.addEventListener(event_name, handler); + // } } - SetText(edit: SetText) { - this.nodes[edit.root].textContent = edit.text; + + RemoveEventListener(root: number, event_name: string, scope: number) { + // } - SetAttribute(edit: SetAttribute) { - // console.log("setting attr", edit); - const name = edit.field; - const value = edit.value; - const ns = edit.ns; - const node = this.nodes[edit.root]; + + 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") { @@ -477,10 +473,9 @@ class Interpreter { } } } - RemoveAttribute(edit: RemoveAttribute) { - const name = edit.name; + RemoveAttribute(root: number, name: string) { - const node = this.nodes[edit.root]; + const node = this.nodes[root]; node.removeAttribute(name); if (name === "value") { @@ -501,79 +496,87 @@ class Interpreter { this.stack.push(this.root); for (let edit of edits) { - switch (edit.type) { - case "AppendChildren": - this.AppendChildren(edit); - break; - case "ReplaceWith": - this.ReplaceWith(edit); - break; - case "InsertAfter": - this.InsertAfter(edit); - break; - case "InsertBefore": - this.InsertBefore(edit); - break; - case "Remove": - this.Remove(edit); - break; - case "CreateTextNode": - this.CreateTextNode(edit); - break; - case "CreateElement": - this.CreateElement(edit); - break; - case "CreateElementNs": - this.CreateElementNs(edit); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit); - break; - case "NewEventListener": - // todo: only on desktop should we make our own handler - let handler = (event: Event) => { - const target = event.target as Element | null; - console.log("event", event); - if (target != null) { + this.handleEdit(edit); + } + } - const real_id = target.getAttribute(`dioxus-id`); + 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, edit.scope); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + let handler = (event: Event) => { + const target = event.target as Element | null; + console.log("event", event); + if (target != null) { - const should_prevent_default = target.getAttribute( - `dioxus-prevent-default` - ); + const real_id = target.getAttribute(`dioxus-id`); - let contents = serialize_event(event); + const should_prevent_default = target.getAttribute( + `dioxus-prevent-default` + ); - if (should_prevent_default === `on${event.type}`) { - event.preventDefault(); - } + let contents = serialize_event(event); - if (real_id == null) { - return; - } - - window.rpc.call("user_event", { - event: (edit as NewEventListener).event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); + if (should_prevent_default === `on${event.type}`) { + event.preventDefault(); } - }; - this.NewEventListener(edit, handler); - break; - case "SetText": - this.SetText(edit); - break; - case "SetAttribute": - this.SetAttribute(edit); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit); - break; - } + + if (real_id == null) { + return; + } + + window.rpc.call("user_event", { + event: (edit as NewEventListener).event_name, + mounted_dom_id: parseInt(real_id), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root); + // this.NewEventListener(edit, 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; } } } @@ -632,5 +635,3 @@ declare global { type Edits = DomEdit[]; - -main(); diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json index 698781908..8f39daddf 100644 --- a/packages/jsinterpreter/tsconfig.json +++ b/packages/jsinterpreter/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "target": "es5", - // "module": "None", "module": "commonjs", "strict": true, "outDir": "dist", diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 868c5d7b0..52a78e631 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -11,8 +11,8 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7"} -dioxus-html = { path = "../html", version ="^0.1.4"} +dioxus-core = { path = "../core", version = "^0.1.7" } +dioxus-html = { path = "../html", version = "^0.1.4" } js-sys = "0.3" wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } lazy_static = "1.4.0" diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs new file mode 100644 index 000000000..d65fc1300 --- /dev/null +++ b/packages/web/src/bindings.rs @@ -0,0 +1,59 @@ +use dioxus_core::DomEdit; +use wasm_bindgen::{convert::IntoWasmAbi, describe::WasmDescribe, prelude::*}; +use web_sys::{Element, HtmlElement, Node}; + +#[wasm_bindgen(module = "/src/interpreter.js")] +extern "C" { + pub type Interpreter; + + #[wasm_bindgen(constructor)] + pub fn new(arg: Element) -> Interpreter; + + #[wasm_bindgen(method)] + pub fn set_node(this: &Interpreter, id: usize, node: Node); + + #[wasm_bindgen(method)] + pub fn PushRoot(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn AppendChildren(this: &Interpreter, many: u32); + + #[wasm_bindgen(method)] + pub fn ReplaceWith(this: &Interpreter, root: u64, m: u32); + + #[wasm_bindgen(method)] + pub fn InsertAfter(this: &Interpreter, root: u64, n: u32); + + #[wasm_bindgen(method)] + pub fn InsertBefore(this: &Interpreter, root: u64, n: u32); + + #[wasm_bindgen(method)] + pub fn Remove(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateElement(this: &Interpreter, tag: &str, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateElementNs(this: &Interpreter, tag: &str, root: u64, ns: &str); + + #[wasm_bindgen(method)] + pub fn CreatePlaceholder(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn NewEventListener(this: &Interpreter, name: &str, scope: usize, root: u64); + + #[wasm_bindgen(method)] + pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); + + #[wasm_bindgen(method)] + pub fn SetText(this: &Interpreter, root: u64, text: &str); + + #[wasm_bindgen(method)] + pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>); + + #[wasm_bindgen(method)] + pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str); +} diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 364f1a1d7..918af4c4a 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,10 +7,11 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> +use crate::bindings::Interpreter; use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; use fxhash::FxHashMap; use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; -use wasm_bindgen::{closure::Closure, JsCast}; +use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use web_sys::{ CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, HtmlTextAreaElement, Node, @@ -19,13 +20,10 @@ use web_sys::{ use crate::{nodeslab::NodeSlab, WebConfig}; pub struct WebsysDom { - stack: Stack, - - /// A map from ElementID (index) to Node - pub(crate) nodes: NodeSlab, - document: Document, + pub interpreter: Interpreter, + pub(crate) root: Element, sender_callback: Rc, @@ -42,7 +40,6 @@ impl WebsysDom { pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { let document = load_document(); - let nodes = NodeSlab::new(2000); let listeners = FxHashMap::default(); let mut stack = Stack::with_capacity(10); @@ -52,8 +49,7 @@ impl WebsysDom { stack.push(root_node); Self { - stack, - nodes, + interpreter: Interpreter::new(root.clone()), listeners, document, sender_callback, @@ -64,399 +60,38 @@ impl WebsysDom { pub fn apply_edits(&mut self, mut edits: Vec) { for edit in edits.drain(..) { match edit { - DomEdit::PushRoot { root } => self.push(root), - DomEdit::AppendChildren { many } => self.append_children(many), - DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), - DomEdit::Remove { root } => self.remove(root), - DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id), - DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id), - DomEdit::CreateElementNs { tag, root: id, ns } => { - self.create_element(tag, Some(ns), id) + DomEdit::PushRoot { root } => self.interpreter.PushRoot(root), + DomEdit::AppendChildren { many } => self.interpreter.AppendChildren(many), + DomEdit::ReplaceWith { root, m } => self.interpreter.ReplaceWith(root, m), + DomEdit::InsertAfter { root, n } => self.interpreter.InsertAfter(root, n), + DomEdit::InsertBefore { root, n } => self.interpreter.InsertBefore(root, n), + DomEdit::Remove { root } => self.interpreter.Remove(root), + DomEdit::CreateTextNode { text, root } => { + self.interpreter.CreateTextNode(text, root) } - DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), + DomEdit::CreateElement { tag, root } => self.interpreter.CreateElement(tag, root), + DomEdit::CreateElementNs { tag, root, ns } => { + self.interpreter.CreateElementNs(tag, root, ns) + } + DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root), DomEdit::NewEventListener { event_name, scope, - root: mounted_node_id, - } => self.new_event_listener(event_name, scope, mounted_node_id), - - DomEdit::RemoveEventListener { event, root } => { - self.remove_event_listener(event, root) + root, + } => self.interpreter.NewEventListener(event_name, scope.0, root), + DomEdit::RemoveEventListener { root, event } => { + self.interpreter.RemoveEventListener(root, event) } - - DomEdit::SetText { text, root } => self.set_text(text, root), + DomEdit::SetText { root, text } => self.interpreter.SetText(root, text), DomEdit::SetAttribute { + root, field, value, ns, - root, - } => self.set_attribute(field, value, ns, root), - DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), - - DomEdit::InsertAfter { n, root } => self.insert_after(n, root), - DomEdit::InsertBefore { n, root } => self.insert_before(n, root), - } - } - } - fn push(&mut self, root: u64) { - let key = root as usize; - let domnode = &self.nodes[key]; - - let real_node: Node = match domnode { - Some(n) => n.clone(), - None => todo!(), - }; - - self.stack.push(real_node); - } - - fn append_children(&mut self, many: u32) { - let root: Node = self - .stack - .list - .get(self.stack.list.len() - (1 + many as usize)) - .unwrap() - .clone(); - - // We need to make sure to add comments between text nodes - // We ensure that the text siblings are patched by preventing the browser from merging - // neighboring text nodes. Originally inspired by some of React's work from 2016. - // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes - // -> https://github.com/facebook/react/pull/5753 - /* - todo: we need to track this for replacing/insert after/etc - */ - let mut last_node_was_text = false; - - for child in self - .stack - .list - .drain((self.stack.list.len() - many as usize)..) - { - if child.dyn_ref::().is_some() { - if last_node_was_text { - let comment_node = self - .document - .create_comment("dioxus") - .dyn_into::() - .unwrap(); - root.append_child(&comment_node).unwrap(); + } => self.interpreter.SetAttribute(root, field, value, ns), + DomEdit::RemoveAttribute { root, name } => { + self.interpreter.RemoveAttribute(root, name) } - last_node_was_text = true; - } else { - last_node_was_text = false; - } - root.append_child(&child).unwrap(); - } - } - - fn replace_with(&mut self, m: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - m as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } - } - - fn remove(&mut self, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(element) = node.dyn_ref::() { - element.remove(); - } else { - if let Some(parent) = node.parent_node() { - parent.remove_child(&node).unwrap(); - } - } - } - - fn create_placeholder(&mut self, id: u64) { - self.create_element("pre", None, id); - self.set_attribute("hidden", "", None, id); - } - - fn create_text_node(&mut self, text: &str, id: u64) { - let textnode = self - .document - .create_text_node(text) - .dyn_into::() - .unwrap(); - - self.stack.push(textnode.clone()); - - self.nodes[(id as usize)] = Some(textnode); - } - - fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { - let tag = wasm_bindgen::intern(tag); - - let el = match ns { - Some(ns) => self - .document - .create_element_ns(Some(ns), tag) - .unwrap() - .dyn_into::() - .unwrap(), - None => self - .document - .create_element(tag) - .unwrap() - .dyn_into::() - .unwrap(), - }; - - use smallstr::SmallString; - use std::fmt::Write; - - let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); - write!(s, "{}", id).unwrap(); - - let el2 = el.dyn_ref::().unwrap(); - el2.set_attribute("dioxus-id", s.as_str()).unwrap(); - - self.stack.push(el.clone()); - self.nodes[(id as usize)] = Some(el); - } - - fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { - let event = wasm_bindgen::intern(event); - - // attach the correct attributes to the element - // these will be used by accessing the event's target - // This ensures we only ever have one handler attached to the root, but decide - // dynamically when we want to call a listener. - - let el = self.stack.top(); - - let el = el.dyn_ref::().unwrap(); - - el.set_attribute("dioxus-event", event).unwrap(); - - // Register the callback to decode - - if let Some(entry) = self.listeners.get_mut(event) { - entry.0 += 1; - } else { - let trigger = self.sender_callback.clone(); - - let c: Box = Box::new(move |event: &web_sys::Event| { - // "Result" cannot be received from JS - // Instead, we just build and immediately execute a closure that returns result - match decode_trigger(event) { - Ok(synthetic_event) => { - let target = event.target().unwrap(); - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } - } - } - - trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) - } - Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), - }; - }); - - let handler = Closure::wrap(c); - - self.root - .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) - .unwrap(); - - // Increment the listeners - self.listeners.insert(event.into(), (1, handler)); - } - } - - fn remove_event_listener(&mut self, _event: &str, _root: u64) { - todo!() - } - - fn set_text(&mut self, text: &str, root: u64) { - let el = self.nodes[root as usize].as_ref().unwrap(); - el.set_text_content(Some(text)) - } - - fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if ns == Some("style") { - if let Some(el) = node.dyn_ref::() { - let el = el.dyn_ref::().unwrap(); - let style_dc: CssStyleDeclaration = el.style(); - style_dc.set_property(name, value).unwrap(); - } - } else { - let fallback = || { - let el = node.dyn_ref::().unwrap(); - el.set_attribute(name, value).unwrap() - }; - match name { - "dangerous_inner_html" => { - if let Some(el) = node.dyn_ref::() { - el.set_inner_html(value); - } - } - "value" => { - if let Some(input) = node.dyn_ref::() { - /* - if the attribute being set is the same as the value of the input, then don't bother setting it. - This is used in controlled components to keep the cursor in the right spot. - - this logic should be moved into the virtualdom since we have the notion of "volatile" - */ - if input.value() != value { - input.set_value(value); - } - } else if let Some(node) = node.dyn_ref::() { - if name == "value" { - node.set_value(value); - } - } else { - fallback(); - } - } - "checked" => { - if let Some(input) = node.dyn_ref::() { - match value { - "true" => input.set_checked(true), - "false" => input.set_checked(false), - _ => fallback(), - } - } else { - fallback(); - } - } - "selected" => { - if let Some(node) = node.dyn_ref::() { - node.set_selected(true); - } else { - fallback(); - } - } - _ => { - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if value == "false" { - if let Some(el) = node.dyn_ref::() { - match name { - "allowfullscreen" - | "allowpaymentrequest" - | "async" - | "autofocus" - | "autoplay" - | "checked" - | "controls" - | "default" - | "defer" - | "disabled" - | "formnovalidate" - | "hidden" - | "ismap" - | "itemscope" - | "loop" - | "multiple" - | "muted" - | "nomodule" - | "novalidate" - | "open" - | "playsinline" - | "readonly" - | "required" - | "reversed" - | "selected" - | "truespeed" => { - let _ = el.remove_attribute(name); - } - _ => { - let _ = el.set_attribute(name, value); - } - }; - } - } else { - fallback(); - } - } - } - } - } - - fn remove_attribute(&mut self, name: &str, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(node) = node.dyn_ref::() { - node.remove_attribute(name).unwrap(); - } - if let Some(node) = node.dyn_ref::() { - // Some attributes are "volatile" and don't work through `removeAttribute`. - if name == "value" { - node.set_value(""); - } - if name == "checked" { - node.set_checked(false); - } - } - - if let Some(node) = node.dyn_ref::() { - if name == "selected" { - node.set_selected(true); - } - } - } - - fn insert_after(&mut self, n: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } - } - - fn insert_before(&mut self, n: u32, root: u64) { - let anchor = self.nodes[root as usize].as_ref().unwrap(); - - if n == 1 { - let before = self.stack.pop(); - - anchor - .parent_node() - .unwrap() - .insert_before(&before, Some(&anchor)) - .unwrap(); - } else { - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); } } } diff --git a/packages/web/src/interpreter.js b/packages/web/src/interpreter.js new file mode 100644 index 000000000..1d0f14c68 --- /dev/null +++ b/packages/web/src/interpreter.js @@ -0,0 +1,529 @@ +"use strict"; +var exports = {}; +exports.__esModule = true; +exports.Interpreter = void 0; +function serialize_event(event) { + var _a, _b; + switch (event.type) { + case "copy": + case "cut": + case "past": + 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_1 = event.target; + var value_1 = + (_b = target_1.value) !== null && _b !== void 0 + ? _b + : target_1.textContent; + if (target_1.type == "checkbox") { + value_1 = target_1.checked ? "true" : "false"; + } + return { + value: value_1, + }; + } + 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_1 = _d.altKey, + button = _d.button, + buttons = _d.buttons, + clientX = _d.clientX, + clientY = _d.clientY, + ctrlKey_1 = _d.ctrlKey, + metaKey_1 = _d.metaKey, + pageX = _d.pageX, + pageY = _d.pageY, + screenX_1 = _d.screenX, + screenY_1 = _d.screenY, + shiftKey_1 = _d.shiftKey; + return { + alt_key: altKey_1, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_1, + meta_key: metaKey_1, + page_x: pageX, + page_y: pageY, + screen_x: screenX_1, + screen_y: screenY_1, + shift_key: shiftKey_1, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + var _e = event, + altKey_2 = _e.altKey, + button = _e.button, + buttons = _e.buttons, + clientX = _e.clientX, + clientY = _e.clientY, + ctrlKey_2 = _e.ctrlKey, + metaKey_2 = _e.metaKey, + pageX = _e.pageX, + pageY = _e.pageY, + screenX_2 = _e.screenX, + screenY_2 = _e.screenY, + shiftKey_2 = _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_2, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_2, + meta_key: metaKey_2, + page_x: pageX, + page_y: pageY, + screen_x: screenX_2, + screen_y: screenY_2, + shift_key: shiftKey_2, + 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_3 = _f.altKey, + ctrlKey_3 = _f.ctrlKey, + metaKey_3 = _f.metaKey, + shiftKey_3 = _f.shiftKey; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey_3, + ctrl_key: ctrlKey_3, + meta_key: metaKey_3, + shift_key: shiftKey_3, + }; + } + 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, +}; +export var Interpreter = /** @class */ (function () { + function Interpreter(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + 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) { + 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]) { + // this.listeners[event_name] = handler; + // this.root.addEventListener(event_name, handler); + // } + }; + Interpreter.prototype.RemoveEventListener = function ( + root, + event_name, + scope + ) { + // + }; + 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) { + console.log("handling edits ", 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, edit.scope); + 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 real_id = target.getAttribute("dioxus-id"); + var should_prevent_default = target.getAttribute( + "dioxus-prevent-default" + ); + var contents = serialize_event(event); + if (should_prevent_default === "on".concat(event.type)) { + event.preventDefault(); + } + if (real_id == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(real_id), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root); + // this.NewEventListener(edit, 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; +})(); +exports.Interpreter = Interpreter; +function main() { + var root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 67d52f4fe..ba9398276 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -61,6 +61,7 @@ pub use dioxus_core as dioxus; use dioxus_core::prelude::Component; use futures_util::FutureExt; +pub(crate) mod bindings; mod cache; mod cfg; mod dom; diff --git a/packages/web/src/olddom.rs b/packages/web/src/olddom.rs new file mode 100644 index 000000000..364f1a1d7 --- /dev/null +++ b/packages/web/src/olddom.rs @@ -0,0 +1,789 @@ +//! Implementation of a renderer for Dioxus on the web. +//! +//! Oustanding todos: +//! - Removing event listeners (delegation) +//! - Passive event listeners +//! - no-op event listener patch for safari +//! - tests to ensure dyn_into works for various event types. +//! - Partial delegation?> + +use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; +use fxhash::FxHashMap; +use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{ + CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, + HtmlOptionElement, HtmlTextAreaElement, Node, +}; + +use crate::{nodeslab::NodeSlab, WebConfig}; + +pub struct WebsysDom { + stack: Stack, + + /// A map from ElementID (index) to Node + pub(crate) nodes: NodeSlab, + + document: Document, + + pub(crate) root: Element, + + sender_callback: Rc, + + // map of listener types to number of those listeners + // This is roughly a delegater + // TODO: check how infero delegates its events - some are more performant + listeners: FxHashMap<&'static str, ListenerEntry>, +} + +type ListenerEntry = (usize, Closure); + +impl WebsysDom { + pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { + let document = load_document(); + + let nodes = NodeSlab::new(2000); + let listeners = FxHashMap::default(); + + let mut stack = Stack::with_capacity(10); + + let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); + let root_node = root.clone().dyn_into::().unwrap(); + stack.push(root_node); + + Self { + stack, + nodes, + listeners, + document, + sender_callback, + root, + } + } + + pub fn apply_edits(&mut self, mut edits: Vec) { + for edit in edits.drain(..) { + match edit { + DomEdit::PushRoot { root } => self.push(root), + DomEdit::AppendChildren { many } => self.append_children(many), + DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), + DomEdit::Remove { root } => self.remove(root), + DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id), + DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id), + DomEdit::CreateElementNs { tag, root: id, ns } => { + self.create_element(tag, Some(ns), id) + } + DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), + DomEdit::NewEventListener { + event_name, + scope, + root: mounted_node_id, + } => self.new_event_listener(event_name, scope, mounted_node_id), + + DomEdit::RemoveEventListener { event, root } => { + self.remove_event_listener(event, root) + } + + DomEdit::SetText { text, root } => self.set_text(text, root), + DomEdit::SetAttribute { + field, + value, + ns, + root, + } => self.set_attribute(field, value, ns, root), + DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), + + DomEdit::InsertAfter { n, root } => self.insert_after(n, root), + DomEdit::InsertBefore { n, root } => self.insert_before(n, root), + } + } + } + fn push(&mut self, root: u64) { + let key = root as usize; + let domnode = &self.nodes[key]; + + let real_node: Node = match domnode { + Some(n) => n.clone(), + None => todo!(), + }; + + self.stack.push(real_node); + } + + fn append_children(&mut self, many: u32) { + let root: Node = self + .stack + .list + .get(self.stack.list.len() - (1 + many as usize)) + .unwrap() + .clone(); + + // We need to make sure to add comments between text nodes + // We ensure that the text siblings are patched by preventing the browser from merging + // neighboring text nodes. Originally inspired by some of React's work from 2016. + // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes + // -> https://github.com/facebook/react/pull/5753 + /* + todo: we need to track this for replacing/insert after/etc + */ + let mut last_node_was_text = false; + + for child in self + .stack + .list + .drain((self.stack.list.len() - many as usize)..) + { + if child.dyn_ref::().is_some() { + if last_node_was_text { + let comment_node = self + .document + .create_comment("dioxus") + .dyn_into::() + .unwrap(); + root.append_child(&comment_node).unwrap(); + } + last_node_was_text = true; + } else { + last_node_was_text = false; + } + root.append_child(&child).unwrap(); + } + } + + fn replace_with(&mut self, m: u32, root: u64) { + let old = self.nodes[root as usize].as_ref().unwrap(); + + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - m as usize)..) + .collect(); + + if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } + } + + fn remove(&mut self, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if let Some(element) = node.dyn_ref::() { + element.remove(); + } else { + if let Some(parent) = node.parent_node() { + parent.remove_child(&node).unwrap(); + } + } + } + + fn create_placeholder(&mut self, id: u64) { + self.create_element("pre", None, id); + self.set_attribute("hidden", "", None, id); + } + + fn create_text_node(&mut self, text: &str, id: u64) { + let textnode = self + .document + .create_text_node(text) + .dyn_into::() + .unwrap(); + + self.stack.push(textnode.clone()); + + self.nodes[(id as usize)] = Some(textnode); + } + + fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { + let tag = wasm_bindgen::intern(tag); + + let el = match ns { + Some(ns) => self + .document + .create_element_ns(Some(ns), tag) + .unwrap() + .dyn_into::() + .unwrap(), + None => self + .document + .create_element(tag) + .unwrap() + .dyn_into::() + .unwrap(), + }; + + use smallstr::SmallString; + use std::fmt::Write; + + let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); + write!(s, "{}", id).unwrap(); + + let el2 = el.dyn_ref::().unwrap(); + el2.set_attribute("dioxus-id", s.as_str()).unwrap(); + + self.stack.push(el.clone()); + self.nodes[(id as usize)] = Some(el); + } + + fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { + let event = wasm_bindgen::intern(event); + + // attach the correct attributes to the element + // these will be used by accessing the event's target + // This ensures we only ever have one handler attached to the root, but decide + // dynamically when we want to call a listener. + + let el = self.stack.top(); + + let el = el.dyn_ref::().unwrap(); + + el.set_attribute("dioxus-event", event).unwrap(); + + // Register the callback to decode + + if let Some(entry) = self.listeners.get_mut(event) { + entry.0 += 1; + } else { + let trigger = self.sender_callback.clone(); + + let c: Box = Box::new(move |event: &web_sys::Event| { + // "Result" cannot be received from JS + // Instead, we just build and immediately execute a closure that returns result + match decode_trigger(event) { + Ok(synthetic_event) => { + let target = event.target().unwrap(); + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); + } + } + } + + trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) + } + Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), + }; + }); + + let handler = Closure::wrap(c); + + self.root + .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) + .unwrap(); + + // Increment the listeners + self.listeners.insert(event.into(), (1, handler)); + } + } + + fn remove_event_listener(&mut self, _event: &str, _root: u64) { + todo!() + } + + fn set_text(&mut self, text: &str, root: u64) { + let el = self.nodes[root as usize].as_ref().unwrap(); + el.set_text_content(Some(text)) + } + + fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if ns == Some("style") { + if let Some(el) = node.dyn_ref::() { + let el = el.dyn_ref::().unwrap(); + let style_dc: CssStyleDeclaration = el.style(); + style_dc.set_property(name, value).unwrap(); + } + } else { + let fallback = || { + let el = node.dyn_ref::().unwrap(); + el.set_attribute(name, value).unwrap() + }; + match name { + "dangerous_inner_html" => { + if let Some(el) = node.dyn_ref::() { + el.set_inner_html(value); + } + } + "value" => { + if let Some(input) = node.dyn_ref::() { + /* + if the attribute being set is the same as the value of the input, then don't bother setting it. + This is used in controlled components to keep the cursor in the right spot. + + this logic should be moved into the virtualdom since we have the notion of "volatile" + */ + if input.value() != value { + input.set_value(value); + } + } else if let Some(node) = node.dyn_ref::() { + if name == "value" { + node.set_value(value); + } + } else { + fallback(); + } + } + "checked" => { + if let Some(input) = node.dyn_ref::() { + match value { + "true" => input.set_checked(true), + "false" => input.set_checked(false), + _ => fallback(), + } + } else { + fallback(); + } + } + "selected" => { + if let Some(node) = node.dyn_ref::() { + node.set_selected(true); + } else { + fallback(); + } + } + _ => { + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if value == "false" { + if let Some(el) = node.dyn_ref::() { + match name { + "allowfullscreen" + | "allowpaymentrequest" + | "async" + | "autofocus" + | "autoplay" + | "checked" + | "controls" + | "default" + | "defer" + | "disabled" + | "formnovalidate" + | "hidden" + | "ismap" + | "itemscope" + | "loop" + | "multiple" + | "muted" + | "nomodule" + | "novalidate" + | "open" + | "playsinline" + | "readonly" + | "required" + | "reversed" + | "selected" + | "truespeed" => { + let _ = el.remove_attribute(name); + } + _ => { + let _ = el.set_attribute(name, value); + } + }; + } + } else { + fallback(); + } + } + } + } + } + + fn remove_attribute(&mut self, name: &str, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if let Some(node) = node.dyn_ref::() { + node.remove_attribute(name).unwrap(); + } + if let Some(node) = node.dyn_ref::() { + // Some attributes are "volatile" and don't work through `removeAttribute`. + if name == "value" { + node.set_value(""); + } + if name == "checked" { + node.set_checked(false); + } + } + + if let Some(node) = node.dyn_ref::() { + if name == "selected" { + node.set_selected(true); + } + } + } + + fn insert_after(&mut self, n: u32, root: u64) { + let old = self.nodes[root as usize].as_ref().unwrap(); + + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - n as usize)..) + .collect(); + + if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } + } + + fn insert_before(&mut self, n: u32, root: u64) { + let anchor = self.nodes[root as usize].as_ref().unwrap(); + + if n == 1 { + let before = self.stack.pop(); + + anchor + .parent_node() + .unwrap() + .insert_before(&before, Some(&anchor)) + .unwrap(); + } else { + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - n as usize)..) + .collect(); + + if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } + } + } +} + +#[derive(Debug, Default)] +struct Stack { + list: Vec, +} + +impl Stack { + #[inline] + fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + #[inline] + fn push(&mut self, node: Node) { + self.list.push(node); + } + + #[inline] + fn pop(&mut self) -> Node { + self.list.pop().unwrap() + } + + fn top(&self) -> &Node { + match self.list.last() { + Some(a) => a, + None => panic!("Called 'top' of an empty stack, make sure to push the root first"), + } + } +} + +pub struct DioxusWebsysEvent(web_sys::Event); + +// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread +unsafe impl Send for DioxusWebsysEvent {} +unsafe impl Sync for DioxusWebsysEvent {} + +// todo: some of these events are being casted to the wrong event type. +// We need tests that simulate clicks/etc and make sure every event type works. +fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { + use dioxus_html::on::*; + use dioxus_html::KeyCode; + + match event.type_().as_str() { + "copy" | "cut" | "paste" => Arc::new(ClipboardData {}), + "compositionend" | "compositionstart" | "compositionupdate" => { + let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); + Arc::new(CompositionData { + data: evt.data().unwrap_or_default(), + }) + } + "keydown" | "keypress" | "keyup" => { + let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap(); + Arc::new(KeyboardData { + alt_key: evt.alt_key(), + char_code: evt.char_code(), + key: evt.key(), + key_code: KeyCode::from_raw_code(evt.key_code() as u8), + ctrl_key: evt.ctrl_key(), + locale: "not implemented".to_string(), + location: evt.location() as usize, + meta_key: evt.meta_key(), + repeat: evt.repeat(), + shift_key: evt.shift_key(), + which: evt.which() as usize, + }) + } + "focus" | "blur" => Arc::new(FocusData {}), + + // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy + // don't have a good solution with the serialized event problem + "change" | "input" | "invalid" | "reset" | "submit" => { + let evt: &web_sys::Event = event.dyn_ref().unwrap(); + + let target: web_sys::EventTarget = evt.target().unwrap(); + let value: String = (&target) + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + // todo: special case more input types + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => { + input.value() + } + } + }) + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + // select elements are NOT input events - because - why woudn't they be?? + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlSelectElement| input.value()) + }) + .or_else(|| { + target + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + Arc::new(FormData { value }) + } + "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" + | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" + | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { + let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap(); + Arc::new(MouseData { + alt_key: evt.alt_key(), + button: evt.button(), + buttons: evt.buttons(), + client_x: evt.client_x(), + client_y: evt.client_y(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + screen_x: evt.screen_x(), + screen_y: evt.screen_y(), + shift_key: evt.shift_key(), + page_x: evt.page_x(), + page_y: evt.page_y(), + }) + } + "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" + | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { + let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap(); + Arc::new(PointerData { + alt_key: evt.alt_key(), + button: evt.button(), + buttons: evt.buttons(), + client_x: evt.client_x(), + client_y: evt.client_y(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + page_x: evt.page_x(), + page_y: evt.page_y(), + screen_x: evt.screen_x(), + screen_y: evt.screen_y(), + shift_key: evt.shift_key(), + pointer_id: evt.pointer_id(), + width: evt.width(), + height: evt.height(), + pressure: evt.pressure(), + tangential_pressure: evt.tangential_pressure(), + tilt_x: evt.tilt_x(), + tilt_y: evt.tilt_y(), + twist: evt.twist(), + pointer_type: evt.pointer_type(), + is_primary: evt.is_primary(), + // get_modifier_state: evt.get_modifier_state(), + }) + } + "select" => Arc::new(SelectionData {}), + "touchcancel" | "touchend" | "touchmove" | "touchstart" => { + let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap(); + Arc::new(TouchData { + alt_key: evt.alt_key(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + shift_key: evt.shift_key(), + }) + } + "scroll" => Arc::new(()), + "wheel" => { + let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); + Arc::new(WheelData { + delta_x: evt.delta_x(), + delta_y: evt.delta_y(), + delta_z: evt.delta_z(), + delta_mode: evt.delta_mode(), + }) + } + "animationstart" | "animationend" | "animationiteration" => { + let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap(); + Arc::new(AnimationData { + elapsed_time: evt.elapsed_time(), + animation_name: evt.animation_name(), + pseudo_element: evt.pseudo_element(), + }) + } + "transitionend" => { + let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap(); + Arc::new(TransitionData { + elapsed_time: evt.elapsed_time(), + property_name: evt.property_name(), + pseudo_element: evt.pseudo_element(), + }) + } + "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" + | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" + | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" + | "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}), + "toggle" => Arc::new(ToggleData {}), + _ => Arc::new(()), + } +} + +/// This function decodes a websys event and produces an EventTrigger +/// With the websys implementation, we attach a unique key to the nodes +fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { + use anyhow::Context; + + let target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + + 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, + }) +} + +pub(crate) fn load_document() -> Document { + web_sys::window() + .expect("should have access to the Window") + .document() + .expect("should have access to the Document") +} + +fn event_name_from_typ(typ: &str) -> &'static str { + match typ { + "copy" => "copy", + "cut" => "cut", + "paste" => "paste", + "compositionend" => "compositionend", + "compositionstart" => "compositionstart", + "compositionupdate" => "compositionupdate", + "keydown" => "keydown", + "keypress" => "keypress", + "keyup" => "keyup", + "focus" => "focus", + "blur" => "blur", + "change" => "change", + "input" => "input", + "invalid" => "invalid", + "reset" => "reset", + "submit" => "submit", + "click" => "click", + "contextmenu" => "contextmenu", + "doubleclick" => "doubleclick", + "drag" => "drag", + "dragend" => "dragend", + "dragenter" => "dragenter", + "dragexit" => "dragexit", + "dragleave" => "dragleave", + "dragover" => "dragover", + "dragstart" => "dragstart", + "drop" => "drop", + "mousedown" => "mousedown", + "mouseenter" => "mouseenter", + "mouseleave" => "mouseleave", + "mousemove" => "mousemove", + "mouseout" => "mouseout", + "mouseover" => "mouseover", + "mouseup" => "mouseup", + "pointerdown" => "pointerdown", + "pointermove" => "pointermove", + "pointerup" => "pointerup", + "pointercancel" => "pointercancel", + "gotpointercapture" => "gotpointercapture", + "lostpointercapture" => "lostpointercapture", + "pointerenter" => "pointerenter", + "pointerleave" => "pointerleave", + "pointerover" => "pointerover", + "pointerout" => "pointerout", + "select" => "select", + "touchcancel" => "touchcancel", + "touchend" => "touchend", + "touchmove" => "touchmove", + "touchstart" => "touchstart", + "scroll" => "scroll", + "wheel" => "wheel", + "animationstart" => "animationstart", + "animationend" => "animationend", + "animationiteration" => "animationiteration", + "transitionend" => "transitionend", + "abort" => "abort", + "canplay" => "canplay", + "canplaythrough" => "canplaythrough", + "durationchange" => "durationchange", + "emptied" => "emptied", + "encrypted" => "encrypted", + "ended" => "ended", + "error" => "error", + "loadeddata" => "loadeddata", + "loadedmetadata" => "loadedmetadata", + "loadstart" => "loadstart", + "pause" => "pause", + "play" => "play", + "playing" => "playing", + "progress" => "progress", + "ratechange" => "ratechange", + "seeked" => "seeked", + "seeking" => "seeking", + "stalled" => "stalled", + "suspend" => "suspend", + "timeupdate" => "timeupdate", + "volumechange" => "volumechange", + "waiting" => "waiting", + "toggle" => "toggle", + _ => { + panic!("unsupported event type") + } + } +} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 8993dd58c..25b126036 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -80,7 +80,8 @@ impl WebsysDom { *last_node_was_text = true; - self.nodes[node_id.0] = Some(node); + self.interpreter.set_node(node_id.0, node); + // self.nodes[node_id.0] = Some(node); *cur_place += 1; } @@ -105,7 +106,8 @@ impl WebsysDom { .set_attribute("dioxus-id", s.as_str()) .unwrap(); - self.nodes[node_id.0] = Some(node.clone()); + self.interpreter.set_node(node_id.0, node.clone()); + // self.nodes[node_id.0] = Some(node.clone()); *cur_place += 1; @@ -135,7 +137,9 @@ impl WebsysDom { let cur_place = place.last_mut().unwrap(); let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - self.nodes[node_id.0] = Some(node); + self.interpreter.set_node(node_id.0, node); + + // self.nodes[node_id.0] = Some(node); *cur_place += 1; } From 05331ddd8033f6997d4916179b62f4d62f832988 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 13 Jan 2022 22:51:01 -0500 Subject: [PATCH 3/4] feat: wire up both desktop and web --- packages/desktop/src/index.html | 1 + packages/jsinterpreter/README.md | 8 + packages/jsinterpreter/interpreter.js | 133 ++++--- packages/jsinterpreter/interpreter.ts | 77 ++-- packages/jsinterpreter/tsconfig.json | 14 +- packages/web/src/bindings.rs | 16 +- packages/web/src/cache.rs | 2 +- packages/web/src/cfg.rs | 5 +- packages/web/src/dom.rs | 94 ++--- packages/web/src/interpreter.js | 529 -------------------------- packages/web/src/lib.rs | 1 - packages/web/src/nodeslab.rs | 34 -- packages/web/src/olddom.rs | 68 ++++ packages/web/src/rehydrate.rs | 4 +- packages/web/src/ric_raf.rs | 3 +- 15 files changed, 261 insertions(+), 728 deletions(-) create mode 100644 packages/jsinterpreter/README.md delete mode 100644 packages/web/src/interpreter.js delete mode 100644 packages/web/src/nodeslab.rs diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 888ab8437..c17a63d2a 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -15,5 +15,6 @@ + diff --git a/packages/jsinterpreter/README.md b/packages/jsinterpreter/README.md new file mode 100644 index 000000000..bab57ac16 --- /dev/null +++ b/packages/jsinterpreter/README.md @@ -0,0 +1,8 @@ +# JS Interpreter + +After diffing old and new trees, the Dioxus VirtualDom produces patches that are used to modify the existing Dom. We can send these patches anywhere - including targets without WASM support. + +In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets. + + +For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks. diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index 6139280f8..453b34406 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,23 +1,22 @@ -"use strict"; -exports.__esModule = true; -exports.Interpreter = void 0; function serialize_event(event) { var _a, _b; switch (event.type) { case "copy": case "cut": - case "past": + case "past": { return {}; + } case "compositionend": case "compositionstart": - case "compositionupdate": + case "compositionupdate": { var data = event.data; return { - data: data + data: data, }; + } case "keydown": case "keypress": - case "keyup": + 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, @@ -30,12 +29,14 @@ function serialize_event(event) { location: location_1, repeat: repeat, which: which, - locale: "locale" + locale: "locale", }; + } case "focus": - case "blur": + case "blur": { return {}; - case "change": + } + case "change": { var target = event.target; var value = void 0; if (target.type === "checkbox" || target.type === "radio") { @@ -45,19 +46,20 @@ function serialize_event(event) { value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent; } return { - value: value + value: value, }; + } case "input": case "invalid": case "reset": case "submit": { - var target_1 = event.target; - var value_1 = (_b = target_1.value) !== null && _b !== void 0 ? _b : target_1.textContent; - if (target_1.type == "checkbox") { - value_1 = target_1.checked ? "true" : "false"; + 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_1 + value: value, }; } case "click": @@ -78,20 +80,20 @@ function serialize_event(event) { case "mouseout": case "mouseover": case "mouseup": { - var _d = event, altKey_1 = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey_1 = _d.ctrlKey, metaKey_1 = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey_1 = _d.shiftKey; + 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_1, + alt_key: altKey, button: button, buttons: buttons, client_x: clientX, client_y: clientY, - ctrl_key: ctrlKey_1, - meta_key: metaKey_1, + ctrl_key: ctrlKey, + meta_key: metaKey, page_x: pageX, page_y: pageY, screen_x: screenX_1, screen_y: screenY_1, - shift_key: shiftKey_1 + shift_key: shiftKey, }; } case "pointerdown": @@ -104,20 +106,20 @@ function serialize_event(event) { case "pointerleave": case "pointerover": case "pointerout": { - var _e = event, altKey_2 = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey_2 = _e.ctrlKey, metaKey_2 = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey_2 = _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; + 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_2, + alt_key: altKey, button: button, buttons: buttons, client_x: clientX, client_y: clientY, - ctrl_key: ctrlKey_2, - meta_key: metaKey_2, + ctrl_key: ctrlKey, + meta_key: metaKey, page_x: pageX, page_y: pageY, screen_x: screenX_2, screen_y: screenY_2, - shift_key: shiftKey_2, + shift_key: shiftKey, pointer_id: pointerId, width: width, height: height, @@ -127,35 +129,37 @@ function serialize_event(event) { tilt_y: tiltY, twist: twist, pointer_type: pointerType, - is_primary: isPrimary + is_primary: isPrimary, }; } - case "select": + case "select": { return {}; + } case "touchcancel": case "touchend": case "touchmove": case "touchstart": { - var _f = event, altKey_3 = _f.altKey, ctrlKey_3 = _f.ctrlKey, metaKey_3 = _f.metaKey, shiftKey_3 = _f.shiftKey; + 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_3, - ctrl_key: ctrlKey_3, - meta_key: metaKey_3, - shift_key: shiftKey_3 + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, }; } - case "scroll": + 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 + delta_mode: deltaMode, }; } case "animationstart": @@ -165,7 +169,7 @@ function serialize_event(event) { return { animation_name: animationName, elapsed_time: elapsedTime, - pseudo_element: pseudoElement + pseudo_element: pseudoElement, }; } case "transitionend": { @@ -173,7 +177,7 @@ function serialize_event(event) { return { property_name: propertyName, elapsed_time: elapsedTime, - pseudo_element: pseudoElement + pseudo_element: pseudoElement, }; } case "abort": @@ -198,12 +202,15 @@ function serialize_event(event) { case "suspend": case "timeupdate": case "volumechange": - case "waiting": + case "waiting": { return {}; - case "toggle": + } + case "toggle": { return {}; - default: + } + default: { return {}; + } } } var bool_attrs = { @@ -214,7 +221,7 @@ var bool_attrs = { autoplay: true, checked: true, controls: true, - "default": true, + default: true, defer: true, disabled: true, formnovalidate: true, @@ -232,13 +239,14 @@ var bool_attrs = { required: true, reversed: true, selected: true, - truespeed: 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]; } @@ -303,17 +311,28 @@ var Interpreter = /** @class */ (function () { this.stack.push(el); this.nodes[root] = el; }; - Interpreter.prototype.NewEventListener = function (event_name, scope, root) { - console.log('new event listener', event_name, root, scope); + 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]) { - // this.listeners[event_name] = handler; - // this.root.addEventListener(event_name, handler); - // } + 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, scope) { - // + 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; @@ -369,7 +388,6 @@ var Interpreter = /** @class */ (function () { } }; Interpreter.prototype.handleEdits = function (edits) { - console.log("handling edits ", edits); this.stack.push(this.root); for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { var edit = edits_1[_i]; @@ -409,13 +427,13 @@ var Interpreter = /** @class */ (function () { this.CreatePlaceholder(edit.root); break; case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name, edit.scope); + 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); + // console.log("event", event); if (target != null) { var real_id = target.getAttribute("dioxus-id"); var should_prevent_default = target.getAttribute("dioxus-prevent-default"); @@ -429,12 +447,11 @@ var Interpreter = /** @class */ (function () { window.rpc.call("user_event", { event: edit.event_name, mounted_dom_id: parseInt(real_id), - contents: contents + contents: contents, }); } }; - this.NewEventListener(edit.event_name, edit.scope, edit.root); - // this.NewEventListener(edit, handler); + this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); break; case "SetText": this.SetText(edit.root, edit.text); @@ -449,7 +466,7 @@ var Interpreter = /** @class */ (function () { }; return Interpreter; }()); -exports.Interpreter = Interpreter; +export { Interpreter }; function main() { var root = window.document.getElementById("main"); if (root != null) { @@ -457,4 +474,4 @@ function main() { window.rpc.call("initialize"); } } -main(); +//# sourceMappingURL=interpreter.js.map \ No newline at end of file diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index e3547effb..17617a209 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -1,23 +1,23 @@ - - function serialize_event(event: Event) { switch (event.type) { case "copy": case "cut": - case "past": + case "past": { return {}; + } case "compositionend": case "compositionstart": - case "compositionupdate": + case "compositionupdate": { let { data } = (event as CompositionEvent); return { data, }; + } case "keydown": case "keypress": - case "keyup": + case "keyup": { let { charCode, key, @@ -44,12 +44,14 @@ function serialize_event(event: Event) { which: which, locale: "locale", }; + } case "focus": - case "blur": + case "blur": { return {}; + } - case "change": + case "change": { let target = event.target as HTMLInputElement; let value; if (target.type === "checkbox" || target.type === "radio") { @@ -61,6 +63,7 @@ function serialize_event(event: Event) { return { value: value, }; + } case "input": case "invalid": @@ -187,8 +190,9 @@ function serialize_event(event: Event) { }; } - case "select": + case "select": { return {}; + } case "touchcancel": case "touchend": @@ -211,8 +215,10 @@ function serialize_event(event: Event) { }; } - case "scroll": + case "scroll": { return {}; + } + case "wheel": { const { deltaX, @@ -278,14 +284,17 @@ function serialize_event(event: Event) { case "suspend": case "timeupdate": case "volumechange": - case "waiting": + case "waiting": { return {}; + } - case "toggle": + case "toggle": { return {}; + } - default: + default: { return {}; + } } } @@ -321,7 +330,8 @@ const bool_attrs = { export class Interpreter { root: Element; stack: Element[]; - listeners: { [key: string]: (event: Event) => void }; + listeners: { [key: string]: number }; + handlers: { [key: string]: (evt: Event) => void }; lastNodeWasText: boolean; nodes: Element[]; @@ -329,8 +339,8 @@ export class Interpreter { constructor(root: Element) { this.root = root; this.stack = [root]; - this.listeners = { - }; + this.listeners = {}; + this.handlers = {}; this.lastNodeWasText = false; this.nodes = [root]; } @@ -412,23 +422,36 @@ export class Interpreter { this.nodes[root] = el; } - NewEventListener(event_name: string, scope: number, root: number) { - console.log('new event listener', event_name, root, scope); + 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]) { - // this.listeners[event_name] = handler; - // this.root.addEventListener(event_name, handler); - // } + 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}` + ); - RemoveEventListener(root: number, event_name: string, scope: number) { - // + 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]; + } } @@ -492,7 +515,6 @@ export class Interpreter { } handleEdits(edits: DomEdit[]) { - console.log("handling edits ", edits); this.stack.push(this.root); for (let edit of edits) { @@ -533,13 +555,13 @@ export class Interpreter { this.CreatePlaceholder(edit.root); break; case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name, edit.scope); + this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": // todo: only on desktop should we make our own handler let handler = (event: Event) => { const target = event.target as Element | null; - console.log("event", event); + // console.log("event", event); if (target != null) { const real_id = target.getAttribute(`dioxus-id`); @@ -565,8 +587,7 @@ export class Interpreter { }); } }; - this.NewEventListener(edit.event_name, edit.scope, edit.root); - // this.NewEventListener(edit, handler); + this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); break; case "SetText": this.SetText(edit.root, edit.text); diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json index 8f39daddf..883760881 100644 --- a/packages/jsinterpreter/tsconfig.json +++ b/packages/jsinterpreter/tsconfig.json @@ -1,9 +1,13 @@ { "compilerOptions": { - "target": "es5", - "module": "commonjs", - "strict": true, - "outDir": "dist", - "sourceMap": true + "target": "ES5", + "module": "ESNext", + "lib": [ + "es2015", + "es5", + "es6", + "dom" + ], + "strict": true } } diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index d65fc1300..cc4202886 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -1,8 +1,8 @@ -use dioxus_core::DomEdit; -use wasm_bindgen::{convert::IntoWasmAbi, describe::WasmDescribe, prelude::*}; -use web_sys::{Element, HtmlElement, Node}; +use js_sys::Function; +use wasm_bindgen::prelude::*; +use web_sys::{Element, Node}; -#[wasm_bindgen(module = "/src/interpreter.js")] +#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")] extern "C" { pub type Interpreter; @@ -43,7 +43,13 @@ extern "C" { pub fn CreatePlaceholder(this: &Interpreter, root: u64); #[wasm_bindgen(method)] - pub fn NewEventListener(this: &Interpreter, name: &str, scope: usize, root: u64); + pub fn NewEventListener( + this: &Interpreter, + name: &str, + scope: usize, + root: u64, + handler: &Function, + ); #[wasm_bindgen(method)] pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); diff --git a/packages/web/src/cache.rs b/packages/web/src/cache.rs index 190b505c1..e17203a65 100644 --- a/packages/web/src/cache.rs +++ b/packages/web/src/cache.rs @@ -1,4 +1,4 @@ -pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[ +pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[ // Important tags to dioxus "dioxus-id", "dioxus", diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 0fa0dc48c..cc5d18bf7 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -3,10 +3,9 @@ /// This struct helps configure the specifics of hydration and render destination for WebSys. /// /// # Example +/// /// ```rust, ignore -/// fn main() { -/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot")) -/// } +/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot")) /// ``` pub struct WebConfig { pub(crate) hydrate: bool, diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 918af4c4a..c4bd6a6e5 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -8,51 +8,50 @@ //! - Partial delegation?> use crate::bindings::Interpreter; -use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; -use fxhash::FxHashMap; -use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; -use wasm_bindgen::{closure::Closure, JsCast, JsValue}; -use web_sys::{ - CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, - HtmlOptionElement, HtmlTextAreaElement, Node, -}; +use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; +use js_sys::Function; +use std::{any::Any, rc::Rc, sync::Arc}; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{Document, Element, Event, HtmlElement}; -use crate::{nodeslab::NodeSlab, WebConfig}; +use crate::WebConfig; pub struct WebsysDom { - document: Document, - pub interpreter: Interpreter, pub(crate) root: Element, - sender_callback: Rc, - - // map of listener types to number of those listeners - // This is roughly a delegater - // TODO: check how infero delegates its events - some are more performant - listeners: FxHashMap<&'static str, ListenerEntry>, + handler: Closure, } -type ListenerEntry = (usize, Closure); - impl WebsysDom { pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { - let document = load_document(); + // eventually, we just want to let the interpreter do all the work of decoding events into our event type + let callback: Box = Box::new(move |event: &web_sys::Event| { + if let Ok(synthetic_event) = decode_trigger(event) { + // Try to prevent default if the attribute is set + if let Some(target) = event.target() { + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); + } + } + } + } - let listeners = FxHashMap::default(); - - let mut stack = Stack::with_capacity(10); + sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event)) + } + }); let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); - let root_node = root.clone().dyn_into::().unwrap(); - stack.push(root_node); Self { interpreter: Interpreter::new(root.clone()), - listeners, - document, - sender_callback, + handler: Closure::wrap(callback), root, } } @@ -78,7 +77,12 @@ impl WebsysDom { event_name, scope, root, - } => self.interpreter.NewEventListener(event_name, scope.0, root), + } => { + // + let handler: &Function = self.handler.as_ref().unchecked_ref(); + self.interpreter + .NewEventListener(event_name, scope.0, root, handler); + } DomEdit::RemoveEventListener { root, event } => { self.interpreter.RemoveEventListener(root, event) } @@ -97,37 +101,6 @@ impl WebsysDom { } } -#[derive(Debug, Default)] -struct Stack { - list: Vec, -} - -impl Stack { - #[inline] - fn with_capacity(cap: usize) -> Self { - Stack { - list: Vec::with_capacity(cap), - } - } - - #[inline] - fn push(&mut self, node: Node) { - self.list.push(node); - } - - #[inline] - fn pop(&mut self) -> Node { - self.list.pop().unwrap() - } - - fn top(&self) -> &Node { - match self.list.last() { - Some(a) => a, - None => panic!("Called 'top' of an empty stack, make sure to push the root first"), - } - } -} - pub struct DioxusWebsysEvent(web_sys::Event); // safety: currently the web is not multithreaded and our VirtualDom exists on the same thread @@ -267,6 +240,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc Arc::new(()), "wheel" => { let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); diff --git a/packages/web/src/interpreter.js b/packages/web/src/interpreter.js deleted file mode 100644 index 1d0f14c68..000000000 --- a/packages/web/src/interpreter.js +++ /dev/null @@ -1,529 +0,0 @@ -"use strict"; -var exports = {}; -exports.__esModule = true; -exports.Interpreter = void 0; -function serialize_event(event) { - var _a, _b; - switch (event.type) { - case "copy": - case "cut": - case "past": - 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_1 = event.target; - var value_1 = - (_b = target_1.value) !== null && _b !== void 0 - ? _b - : target_1.textContent; - if (target_1.type == "checkbox") { - value_1 = target_1.checked ? "true" : "false"; - } - return { - value: value_1, - }; - } - 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_1 = _d.altKey, - button = _d.button, - buttons = _d.buttons, - clientX = _d.clientX, - clientY = _d.clientY, - ctrlKey_1 = _d.ctrlKey, - metaKey_1 = _d.metaKey, - pageX = _d.pageX, - pageY = _d.pageY, - screenX_1 = _d.screenX, - screenY_1 = _d.screenY, - shiftKey_1 = _d.shiftKey; - return { - alt_key: altKey_1, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey_1, - meta_key: metaKey_1, - page_x: pageX, - page_y: pageY, - screen_x: screenX_1, - screen_y: screenY_1, - shift_key: shiftKey_1, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - var _e = event, - altKey_2 = _e.altKey, - button = _e.button, - buttons = _e.buttons, - clientX = _e.clientX, - clientY = _e.clientY, - ctrlKey_2 = _e.ctrlKey, - metaKey_2 = _e.metaKey, - pageX = _e.pageX, - pageY = _e.pageY, - screenX_2 = _e.screenX, - screenY_2 = _e.screenY, - shiftKey_2 = _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_2, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey_2, - meta_key: metaKey_2, - page_x: pageX, - page_y: pageY, - screen_x: screenX_2, - screen_y: screenY_2, - shift_key: shiftKey_2, - 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_3 = _f.altKey, - ctrlKey_3 = _f.ctrlKey, - metaKey_3 = _f.metaKey, - shiftKey_3 = _f.shiftKey; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey_3, - ctrl_key: ctrlKey_3, - meta_key: metaKey_3, - shift_key: shiftKey_3, - }; - } - 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, -}; -export var Interpreter = /** @class */ (function () { - function Interpreter(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - 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) { - 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]) { - // this.listeners[event_name] = handler; - // this.root.addEventListener(event_name, handler); - // } - }; - Interpreter.prototype.RemoveEventListener = function ( - root, - event_name, - scope - ) { - // - }; - 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) { - console.log("handling edits ", 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, edit.scope); - 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 real_id = target.getAttribute("dioxus-id"); - var should_prevent_default = target.getAttribute( - "dioxus-prevent-default" - ); - var contents = serialize_event(event); - if (should_prevent_default === "on".concat(event.type)) { - event.preventDefault(); - } - if (real_id == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.scope, edit.root); - // this.NewEventListener(edit, 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; -})(); -exports.Interpreter = Interpreter; -function main() { - var root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index ba9398276..8c0c0c3b4 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -65,7 +65,6 @@ pub(crate) mod bindings; mod cache; mod cfg; mod dom; -mod nodeslab; mod rehydrate; mod ric_raf; diff --git a/packages/web/src/nodeslab.rs b/packages/web/src/nodeslab.rs deleted file mode 100644 index b986bd34e..000000000 --- a/packages/web/src/nodeslab.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! This module provides a mirror of the VirtualDOM Element Slab using a Vector. - -use std::ops::{Index, IndexMut}; -use web_sys::Node; - -pub(crate) struct NodeSlab { - nodes: Vec>, -} - -impl NodeSlab { - pub fn new(capacity: usize) -> NodeSlab { - let nodes = Vec::with_capacity(capacity); - NodeSlab { nodes } - } -} -impl Index for NodeSlab { - type Output = Option; - fn index(&self, index: usize) -> &Self::Output { - &self.nodes[index] - } -} - -impl IndexMut for NodeSlab { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - if index >= self.nodes.capacity() * 3 { - panic!("Trying to mutate an element way too far out of bounds"); - } - - if index + 1 > self.nodes.len() { - self.nodes.resize_with(index + 1, || None); - } - &mut self.nodes[index] - } -} diff --git a/packages/web/src/olddom.rs b/packages/web/src/olddom.rs index 364f1a1d7..8654ced90 100644 --- a/packages/web/src/olddom.rs +++ b/packages/web/src/olddom.rs @@ -787,3 +787,71 @@ fn event_name_from_typ(typ: &str) -> &'static str { } } } + + +//! This module provides a mirror of the VirtualDOM Element Slab using a Vector. + +use std::ops::{Index, IndexMut}; +use web_sys::Node; + +pub(crate) struct NodeSlab { + nodes: Vec>, +} + +impl NodeSlab { + pub fn new(capacity: usize) -> NodeSlab { + let nodes = Vec::with_capacity(capacity); + NodeSlab { nodes } + } +} +impl Index for NodeSlab { + type Output = Option; + fn index(&self, index: usize) -> &Self::Output { + &self.nodes[index] + } +} + +impl IndexMut for NodeSlab { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + if index >= self.nodes.capacity() * 3 { + panic!("Trying to mutate an element way too far out of bounds"); + } + + if index + 1 > self.nodes.len() { + self.nodes.resize_with(index + 1, || None); + } + &mut self.nodes[index] + } +} + + +#[derive(Debug, Default)] +struct Stack { + list: Vec, +} + +impl Stack { + #[inline] + fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + #[inline] + fn push(&mut self, node: Node) { + self.list.push(node); + } + + #[inline] + fn pop(&mut self) -> Node { + self.list.pop().unwrap() + } + + fn top(&self) -> &Node { + match self.list.last() { + Some(a) => a, + None => panic!("Called 'top' of an empty stack, make sure to push the root first"), + } + } +} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 25b126036..b62cba753 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -118,7 +118,7 @@ impl WebsysDom { // we cant have the last node be text let mut last_node_was_text = false; for child in vel.children { - self.rehydrate_single(nodes, place, dom, &child, &mut last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?; } place.pop(); @@ -146,7 +146,7 @@ impl WebsysDom { VNode::Fragment(el) => { for el in el.children { - self.rehydrate_single(nodes, place, dom, &el, last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, el, last_node_was_text)?; } } diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 29c97bcba..2ac0668b9 100644 --- a/packages/web/src/ric_raf.rs +++ b/packages/web/src/ric_raf.rs @@ -68,8 +68,7 @@ impl RafLoop { let ric_fn = self.ric_closure.as_ref().dyn_ref::().unwrap(); let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap(); let deadline = self.ric_receiver.recv().await.unwrap(); - let deadline = TimeoutFuture::new(deadline); - deadline + TimeoutFuture::new(deadline) } pub async fn wait_for_raf(&self) { From 50f90d889affbd355f2f9caa4e5c039ab44500db Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 00:21:23 -0500 Subject: [PATCH 4/4] fix: bump to readd desktop click handling code --- packages/jsinterpreter/interpreter.ts | 40 +++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index 17617a209..533536277 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -558,31 +558,50 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": - // todo: only on desktop should we make our own handler + + + // 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; - // console.log("event", event); + if (target != null) { + const realId = target.getAttribute(`dioxus-id`); - const real_id = target.getAttribute(`dioxus-id`); - - const should_prevent_default = target.getAttribute( - `dioxus-prevent-default` - ); + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); let contents = serialize_event(event); - if (should_prevent_default === `on${event.type}`) { + if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); } - if (real_id == null) { + 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(real_id), + mounted_dom_id: parseInt(realId), contents: contents, }); } @@ -655,4 +674,3 @@ declare global { } -type Edits = DomEdit[];