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 + } +}