wip: web now links against the js interprter code

This commit is contained in:
Jonathan Kelley 2022-01-12 08:57:42 -05:00
parent 5bf6c96f9f
commit 10db6ad65b
10 changed files with 1661 additions and 645 deletions

View file

@ -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) {

View file

@ -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();

View file

@ -1,7 +1,6 @@
{
"compilerOptions": {
"target": "es5",
// "module": "None",
"module": "commonjs",
"strict": true,
"outDir": "dist",

View file

@ -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"

View file

@ -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);
}

View file

@ -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<dyn Fn(SchedulerMsg)>,
@ -42,7 +40,6 @@ impl WebsysDom {
pub fn new(cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> 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<DomEdit>) {
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::<web_sys::Text>().is_some() {
if last_node_was_text {
let comment_node = self
.document
.create_comment("dioxus")
.dyn_into::<Node>()
.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::<Element>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
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>() {
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::<Node>()
.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::<Node>()
.unwrap(),
None => self
.document
.create_element(tag)
.unwrap()
.dyn_into::<Node>()
.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::<Element>().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::<Element>().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<dyn FnMut(&Event)> = 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::<HtmlElement>() {
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::<Element>() {
let el = el.dyn_ref::<HtmlElement>().unwrap();
let style_dc: CssStyleDeclaration = el.style();
style_dc.set_property(name, value).unwrap();
}
} else {
let fallback = || {
let el = node.dyn_ref::<Element>().unwrap();
el.set_attribute(name, value).unwrap()
};
match name {
"dangerous_inner_html" => {
if let Some(el) = node.dyn_ref::<Element>() {
el.set_inner_html(value);
}
}
"value" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
/*
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::<HtmlTextAreaElement>() {
if name == "value" {
node.set_value(value);
}
} else {
fallback();
}
}
"checked" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
match value {
"true" => input.set_checked(true),
"false" => input.set_checked(false),
_ => fallback(),
}
} else {
fallback();
}
}
"selected" => {
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
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::<Element>() {
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::<web_sys::Element>() {
node.remove_attribute(name).unwrap();
}
if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
// 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::<HtmlOptionElement>() {
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::<Element>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
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::<Element>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = anchor.dyn_ref::<web_sys::CharacterData>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = anchor.dyn_ref::<web_sys::DocumentType>() {
el.before_with_node(&arr).unwrap();
}
}
}

View file

@ -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");
}
}

View file

@ -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;

789
packages/web/src/olddom.rs Normal file
View file

@ -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<dyn Fn(SchedulerMsg)>,
// 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<dyn FnMut(&Event)>);
impl WebsysDom {
pub fn new(cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> 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::<Node>().unwrap();
stack.push(root_node);
Self {
stack,
nodes,
listeners,
document,
sender_callback,
root,
}
}
pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
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::<web_sys::Text>().is_some() {
if last_node_was_text {
let comment_node = self
.document
.create_comment("dioxus")
.dyn_into::<Node>()
.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::<Element>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
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>() {
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::<Node>()
.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::<Node>()
.unwrap(),
None => self
.document
.create_element(tag)
.unwrap()
.dyn_into::<Node>()
.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::<Element>().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::<Element>().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<dyn FnMut(&Event)> = 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::<HtmlElement>() {
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::<Element>() {
let el = el.dyn_ref::<HtmlElement>().unwrap();
let style_dc: CssStyleDeclaration = el.style();
style_dc.set_property(name, value).unwrap();
}
} else {
let fallback = || {
let el = node.dyn_ref::<Element>().unwrap();
el.set_attribute(name, value).unwrap()
};
match name {
"dangerous_inner_html" => {
if let Some(el) = node.dyn_ref::<Element>() {
el.set_inner_html(value);
}
}
"value" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
/*
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::<HtmlTextAreaElement>() {
if name == "value" {
node.set_value(value);
}
} else {
fallback();
}
}
"checked" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
match value {
"true" => input.set_checked(true),
"false" => input.set_checked(false),
_ => fallback(),
}
} else {
fallback();
}
}
"selected" => {
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
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::<Element>() {
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::<web_sys::Element>() {
node.remove_attribute(name).unwrap();
}
if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
// 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::<HtmlOptionElement>() {
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::<Element>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
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::<Element>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = anchor.dyn_ref::<web_sys::CharacterData>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = anchor.dyn_ref::<web_sys::DocumentType>() {
el.before_with_node(&arr).unwrap();
}
}
}
}
#[derive(Debug, Default)]
struct Stack {
list: Vec<Node>,
}
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<dyn Any + Send + Sync> {
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::<web_sys::HtmlElement>()
.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<UserEvent> {
use anyhow::Context;
let target = event
.target()
.expect("missing target")
.dyn_into::<Element>()
.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")
}
}
}

View file

@ -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;
}