extend interpreter base class with platform methods

This commit is contained in:
Jonathan Kelley committed 2024-02-27 12:31:11 -08:00
1 parent 7f60010c1e
commit b5447c162f
27 files changed
+1394 -1371

No files matched your search

-80
View File
@@ -1,80 +0,0 @@
export function setAttributeInner(node, field, value, ns) {
const name = field;
if (ns === "style") {
// ????? why do we need to do this
if (node.style === undefined) {
node.style = {};
}
node.style[name] = value;
} else if (!!ns) {
node.setAttributeNS(ns, name, value);
} else {
switch (name) {
case "value":
if (value !== node.value) {
node.value = value;
}
break;
case "initial_value":
node.defaultValue = value;
break;
case "checked":
node.checked = truthy(value);
break;
case "initial_checked":
node.defaultChecked = truthy(value);
break;
case "selected":
node.selected = truthy(value);
break;
case "initial_selected":
node.defaultSelected = truthy(value);
break;
case "dangerous_inner_html":
node.innerHTML = value;
break;
default:
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
if (!truthy(value) && bool_attrs.hasOwnProperty(name)) {
node.removeAttribute(name);
} else {
node.setAttribute(name, value);
}
}
}
}
const bool_attrs = {
allowfullscreen: true,
allowpaymentrequest: true,
async: true,
autofocus: true,
autoplay: true,
checked: true,
controls: true,
default: true,
defer: true,
disabled: true,
formnovalidate: true,
hidden: true,
ismap: true,
itemscope: true,
loop: true,
multiple: true,
muted: true,
nomodule: true,
novalidate: true,
open: true,
playsinline: true,
readonly: true,
required: true,
reversed: true,
selected: true,
truespeed: true,
webkitdirectory: true,
};
function truthy(val) {
return val === "true" || val === true;
}
-792
View File
@@ -1,792 +0,0 @@
class InterpreterConfig {
intercept_link_redirects: boolean;
constructor(intercept_link_redirects: boolean) {
this.intercept_link_redirects = intercept_link_redirects;
}
}
// this handler is only provided on the desktop and liveview implementations since this
// method is not used by the web implementation
async function handler(event, name, bubbles, config) {
let target = event.target;
if (target == null) {
return;
}
const realId = find_real_id(target);
if (realId === null) {
return;
}
prevent_defaults(event, target, config);
let contents = await serialize_event(event);
if (
target.tagName === "FORM" &&
(event.type === "submit" || event.type === "input")
) {
const formData = new FormData(target);
for (let name of formData.keys()) {
const fieldType = target.elements[name].type;
switch (fieldType) {
case "select-multiple":
contents.values[name] = formData.getAll(name);
break;
// add cases for fieldTypes that can hold multiple values here
default:
contents.values[name] = formData.get(name);
break;
}
}
}
if (
target.tagName === "SELECT" &&
event.type === "input"
) {
const selectData = target.options;
contents.values["options"] = [];
for (let i = 0; i < selectData.length; i++) {
let option = selectData[i];
if (option.selected) {
contents.values["options"].push(option.value.toString());
}
}
}
// TODO: this should be liveview only
if (
target.tagName === "INPUT" &&
(event.type === "change" || event.type === "input")
) {
const type = target.getAttribute("type");
if (type === "file") {
async function read_files() {
const files = target.files;
const file_contents = {};
for (let i = 0; i < files.length; i++) {
const file = files[i];
file_contents[file.name] = Array.from(
new Uint8Array(await file.arrayBuffer())
);
}
let file_engine = {
files: file_contents,
};
contents.files = file_engine;
if (realId === null) {
return;
}
const message = window.interpreter.serializeIpcMessage("user_event", {
name: name,
element: parseInt(realId),
data: contents,
bubbles,
});
window.ipc.postMessage(message);
}
read_files();
return;
}
}
window.ipc.postMessage(
window.interpreter.serializeIpcMessage("user_event", {
name: name,
element: parseInt(realId),
data: contents,
bubbles,
})
);
}
// Do our best to prevent the default action of the event
// This should:
// - prevent form submissions from navigating
// - prevent anchor tags from navigating
// - prevent buttons from submitting forms
function prevent_defaults(event, target, config) {
let preventDefaultRequests = null;
// Some events can be triggered on text nodes, which don't have attributes
if (target instanceof Element) {
preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`);
}
if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) {
event.preventDefault();
}
if (event.type === "submit") {
event.preventDefault();
}
// Attempt to intercept if the event is a click
intercept_form_submit(event, target, config, preventDefaultRequests);
}
function intercept_form_submit(event, target, config, preventDefaultRequests) {
if (event.type !== "click") {
return;
}
// todo call prevent default if it's the right type of event
if (!config.intercept_link_redirects) {
return;
}
// prevent buttons in forms from submitting the form
if (target.tagName === "BUTTON" && event.type == "submit") {
event.preventDefault();
}
// If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating
let a_element = target.closest("a");
if (a_element == null) {
return;
}
event.preventDefault();
let elementShouldPreventDefault =
preventDefaultRequests && preventDefaultRequests.includes(`onclick`);
let aElementShouldPreventDefault = a_element.getAttribute(
`dioxus-prevent-default`
);
let linkShouldPreventDefault =
aElementShouldPreventDefault &&
aElementShouldPreventDefault.includes(`onclick`);
if (!elementShouldPreventDefault && !linkShouldPreventDefault) {
const href = a_element.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.ipc.postMessage(
window.interpreter.serializeIpcMessage("browser_open", { href })
);
}
}
}
function find_real_id(target) {
let realId = null;
if (target instanceof Element) {
realId = target.getAttribute(`data-dioxus-id`);
}
// walk the tree to find the real element
while (realId == null) {
// we've reached the root we don't want to send an event
if (target.parentElement === null) {
return;
}
target = target.parentElement;
if (target instanceof Element) {
realId = target.getAttribute(`data-dioxus-id`);
}
}
return realId;
}
class ListenerMap {
constructor(root) {
// bubbling events can listen at the root element
this.global = {};
// non bubbling events listen at the element the listener was created at
this.local = {};
this.root = null;
}
create(event_name, element, bubbles, handler) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = {};
this.global[event_name].active = 1;
this.root.addEventListener(event_name, handler);
} else {
this.global[event_name].active++;
}
}
else {
const id = element.getAttribute("data-dioxus-id");
if (!this.local[id]) {
this.local[id] = {};
}
element.addEventListener(event_name, handler);
}
}
remove(element, event_name, bubbles) {
if (bubbles) {
this.global[event_name].active--;
if (this.global[event_name].active === 0) {
this.root.removeEventListener(event_name, this.global[event_name].callback);
delete this.global[event_name];
}
}
else {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (this.local[id].length === 0) {
delete this.local[id];
}
element.removeEventListener(event_name, this.global[event_name].callback);
}
}
removeAllNonBubbling(element) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id];
}
}
function LoadChild(array) {
// iterate through each number and get that child
node = stack[stack.length - 1];
for (let i = 0; i < array.length; i++) {
end = array[i];
for (node = node.firstChild; end > 0; end--) {
node = node.nextSibling;
}
}
return node;
}
const listeners = new ListenerMap();
let nodes = [];
let stack = [];
let root;
const templates = {};
let node, els, end, k;
function AppendChildren(id, many) {
root = nodes[id];
els = stack.splice(stack.length - many);
for (k = 0; k < many; k++) {
root.appendChild(els[k]);
}
}
window.interpreter = {}
window.interpreter.initialize = function (root) {
nodes = [root];
stack = [root];
listeners.root = root;
}
window.interpreter.getClientRect = function (id) {
const node = nodes[id];
if (!node) {
return;
}
const rect = node.getBoundingClientRect();
return {
type: "GetClientRect",
origin: [rect.x, rect.y],
size: [rect.width, rect.height],
};
}
window.interpreter.scrollTo = function (id, behavior) {
const node = nodes[id];
if (!node) {
return false;
}
node.scrollIntoView({
behavior: behavior,
});
return true;
}
/// Set the focus on the element
window.interpreter.setFocus = function (id, focus) {
const node = nodes[id];
if (!node) {
return false;
}
if (focus) {
node.focus();
} else {
node.blur();
}
return true;
}
function get_mouse_data(event: MouseEvent) {
const {
altKey,
button,
buttons,
clientX,
clientY,
ctrlKey,
metaKey,
offsetX,
offsetY,
pageX,
pageY,
screenX,
screenY,
shiftKey,
} = event;
return {
alt_key: altKey,
button: button,
buttons: buttons,
client_x: clientX,
client_y: clientY,
ctrl_key: ctrlKey,
meta_key: metaKey,
offset_x: offsetX,
offset_y: offsetY,
page_x: pageX,
page_y: pageY,
screen_x: screenX,
screen_y: screenY,
shift_key: shiftKey,
};
}
async function serialize_event(event) {
switch (event.type) {
case "copy":
case "cut":
case "past": {
return {};
}
case "compositionend":
case "compositionstart":
case "compositionupdate": {
let { data } = event;
return {
data,
};
}
case "keydown":
case "keypress":
case "keyup": {
let {
charCode,
isComposing,
key,
altKey,
ctrlKey,
metaKey,
keyCode,
shiftKey,
location,
repeat,
which,
code,
} = event;
return {
char_code: charCode,
is_composing: isComposing,
key: key,
alt_key: altKey,
ctrl_key: ctrlKey,
meta_key: metaKey,
key_code: keyCode,
shift_key: shiftKey,
location: location,
repeat: repeat,
which: which,
code,
};
}
case "focus":
case "blur": {
return {};
}
case "change": {
let target = event.target;
let value;
if (target.type === "checkbox" || target.type === "radio") {
value = target.checked ? "true" : "false";
} else {
value = target.value ?? target.textContent;
}
return {
value: value,
values: {},
};
}
case "input":
case "invalid":
case "reset":
case "submit": {
let target = event.target;
let value = target.value ?? target.textContent;
if (target.type === "checkbox") {
value = target.checked ? "true" : "false";
}
return {
value: value,
values: {},
};
}
case "drag":
case "dragend":
case "dragenter":
case "dragexit":
case "dragleave":
case "dragover":
case "dragstart":
case "drop": {
let files = [];
if (event.dataTransfer && event.dataTransfer.files) {
files = ["a", "b", "c"];
// files = await serializeFileList(event.dataTransfer.files);
}
return { mouse: get_mouse_data(event), files };
}
case "click":
case "contextmenu":
case "doubleclick":
case "dblclick":
case "mousedown":
case "mouseenter":
case "mouseleave":
case "mousemove":
case "mouseout":
case "mouseover":
case "mouseup": {
return get_mouse_data(event);
}
case "pointerdown":
case "pointermove":
case "pointerup":
case "pointercancel":
case "gotpointercapture":
case "lostpointercapture":
case "pointerenter":
case "pointerleave":
case "pointerover":
case "pointerout": {
const {
altKey,
button,
buttons,
clientX,
clientY,
ctrlKey,
metaKey,
pageX,
pageY,
screenX,
screenY,
shiftKey,
pointerId,
width,
height,
pressure,
tangentialPressure,
tiltX,
tiltY,
twist,
pointerType,
isPrimary,
} = event;
return {
alt_key: altKey,
button: button,
buttons: buttons,
client_x: clientX,
client_y: clientY,
ctrl_key: ctrlKey,
meta_key: metaKey,
page_x: pageX,
page_y: pageY,
screen_x: screenX,
screen_y: screenY,
shift_key: shiftKey,
pointer_id: pointerId,
width: width,
height: height,
pressure: pressure,
tangential_pressure: tangentialPressure,
tilt_x: tiltX,
tilt_y: tiltY,
twist: twist,
pointer_type: pointerType,
is_primary: isPrimary,
};
}
case "select": {
return {};
}
case "touchcancel":
case "touchend":
case "touchmove":
case "touchstart": {
const { altKey, ctrlKey, metaKey, shiftKey } = event;
return {
// changed_touches: event.changedTouches,
// target_touches: event.targetTouches,
// touches: event.touches,
alt_key: altKey,
ctrl_key: ctrlKey,
meta_key: metaKey,
shift_key: shiftKey,
};
}
case "scroll": {
return {};
}
case "wheel": {
const { deltaX, deltaY, deltaZ, deltaMode } = event;
return {
delta_x: deltaX,
delta_y: deltaY,
delta_z: deltaZ,
delta_mode: deltaMode,
};
}
case "animationstart":
case "animationend":
case "animationiteration": {
const { animationName, elapsedTime, pseudoElement } = event;
return {
animation_name: animationName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "transitionend": {
const { propertyName, elapsedTime, pseudoElement } = event;
return {
property_name: propertyName,
elapsed_time: elapsedTime,
pseudo_element: pseudoElement,
};
}
case "abort":
case "canplay":
case "canplaythrough":
case "durationchange":
case "emptied":
case "encrypted":
case "ended":
case "error":
case "loadeddata":
case "loadedmetadata":
case "loadstart":
case "pause":
case "play":
case "playing":
case "progress":
case "ratechange":
case "seeked":
case "seeking":
case "stalled":
case "suspend":
case "timeupdate":
case "volumechange":
case "waiting": {
return {};
}
case "toggle": {
return {};
}
default: {
return {};
}
}
}
window.interpreter.serializeIpcMessage = function (method, params = {}) {
return JSON.stringify({ method, params });
}
function is_element_node(node) {
return node.nodeType == 1;
}
function event_bubbles(event) {
switch (event) {
case "copy":
return true;
case "cut":
return true;
case "paste":
return true;
case "compositionend":
return true;
case "compositionstart":
return true;
case "compositionupdate":
return true;
case "keydown":
return true;
case "keypress":
return true;
case "keyup":
return true;
case "focus":
return false;
case "focusout":
return true;
case "focusin":
return true;
case "blur":
return false;
case "change":
return true;
case "input":
return true;
case "invalid":
return true;
case "reset":
return true;
case "submit":
return true;
case "click":
return true;
case "contextmenu":
return true;
case "doubleclick":
return true;
case "dblclick":
return true;
case "drag":
return true;
case "dragend":
return true;
case "dragenter":
return false;
case "dragexit":
return false;
case "dragleave":
return true;
case "dragover":
return true;
case "dragstart":
return true;
case "drop":
return true;
case "mousedown":
return true;
case "mouseenter":
return false;
case "mouseleave":
return false;
case "mousemove":
return true;
case "mouseout":
return true;
case "scroll":
return false;
case "mouseover":
return true;
case "mouseup":
return true;
case "pointerdown":
return true;
case "pointermove":
return true;
case "pointerup":
return true;
case "pointercancel":
return true;
case "gotpointercapture":
return true;
case "lostpointercapture":
return true;
case "pointerenter":
return false;
case "pointerleave":
return false;
case "pointerover":
return true;
case "pointerout":
return true;
case "select":
return true;
case "touchcancel":
return true;
case "touchend":
return true;
case "touchmove":
return true;
case "touchstart":
return true;
case "wheel":
return true;
case "abort":
return false;
case "canplay":
return false;
case "canplaythrough":
return false;
case "durationchange":
return false;
case "emptied":
return false;
case "encrypted":
return true;
case "ended":
return false;
case "error":
return false;
case "loadeddata":
case "loadedmetadata":
case "loadstart":
case "load":
return false;
case "pause":
return false;
case "play":
return false;
case "playing":
return false;
case "progress":
return false;
case "ratechange":
return false;
case "seeked":
return false;
case "seeking":
return false;
case "stalled":
return false;
case "suspend":
return false;
case "timeupdate":
return false;
case "volumechange":
return false;
case "waiting":
return false;
case "animationstart":
return true;
case "animationend":
return true;
case "animationiteration":
return true;
case "transitionend":
return true;
case "toggle":
return true;
case "mounted":
return false;
}
return true;
}
+11 -6
View File
@@ -5,14 +5,9 @@
pub static INTERPRETER_JS: &str = include_str!("./gen/interpreter.js");
pub static COMMON_JS: &str = include_str!("./gen/common.js");
#[cfg(feature = "sledgehammer")]
mod sledgehammer_bindings;
#[cfg(feature = "sledgehammer")]
pub use sledgehammer_bindings::*;
#[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))]
mod write_native_mutations;
#[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))]
pub use write_native_mutations::*;
@@ -20,8 +15,18 @@ pub use write_native_mutations::*;
#[cfg(all(feature = "minimal_bindings", feature = "webonly"))]
pub mod minimal_bindings {
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
/// Some useful snippets that we use to share common functionality between the different platforms we support.
///
/// This maintains some sort of consistency between web, desktop, and liveview
#[wasm_bindgen(module = "/src/gen/common.js")]
extern "C" {
/// Set the attribute of the node
pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>);
pub fn collectFormValues(node: JsValue) -> JsValue;
}
}
#[cfg(feature = "sledgehammer")]
pub mod unified_bindings;
@@ -1,407 +0,0 @@
#[cfg(feature = "webonly")]
use js_sys::Function;
#[cfg(feature = "webonly")]
use sledgehammer_bindgen::bindgen;
#[cfg(feature = "webonly")]
use web_sys::Node;
#[cfg(feature = "webonly")]
pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
#[cfg(feature = "webonly")]
#[bindgen(module)]
mod js {
const JS_FILE: &str = "./src/common.js";
const JS: &str = r#"
class ListenerMap {
constructor(root) {
// bubbling events can listen at the root element
this.global = {};
// non bubbling events listen at the element the listener was created at
this.local = {};
this.root = null;
this.handler = null;
}
create(event_name, element, bubbles) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = {};
this.global[event_name].active = 1;
this.root.addEventListener(event_name, this.handler);
} else {
this.global[event_name].active++;
}
}
else {
const id = element.getAttribute("data-dioxus-id");
if (!this.local[id]) {
this.local[id] = {};
}
element.addEventListener(event_name, this.handler);
}
}
remove(element, event_name, bubbles) {
if (bubbles) {
this.global[event_name].active--;
if (this.global[event_name].active === 0) {
this.root.removeEventListener(event_name, this.global[event_name].callback);
delete this.global[event_name];
}
}
else {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (this.local[id].length === 0) {
delete this.local[id];
}
element.removeEventListener(event_name, this.handler);
}
}
removeAllNonBubbling(element) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id];
}
}
function LoadChild(ptr, len) {
// iterate through each number and get that child
node = stack[stack.length - 1];
ptr_end = ptr + len;
for (; ptr < ptr_end; ptr++) {
end = m.getUint8(ptr);
for (node = node.firstChild; end > 0; end--) {
node = node.nextSibling;
}
}
return node;
}
const listeners = new ListenerMap();
let nodes = [];
let stack = [];
let root;
const templates = {};
let node, els, end, ptr_end, k;
export function save_template(nodes, tmpl_id) {
templates[tmpl_id] = nodes;
}
export function hydrate(ids) {
const hydrateNodes = document.querySelectorAll('[data-node-hydration]');
for (let i = 0; i < hydrateNodes.length; i++) {
const hydrateNode = hydrateNodes[i];
const hydration = hydrateNode.getAttribute('data-node-hydration');
const split = hydration.split(',');
const id = ids[parseInt(split[0])];
nodes[id] = hydrateNode;
if (split.length > 1) {
hydrateNode.listening = split.length - 1;
hydrateNode.setAttribute('data-dioxus-id', id);
for (let j = 1; j < split.length; j++) {
const listener = split[j];
const split2 = listener.split(':');
const event_name = split2[0];
const bubbles = split2[1] === '1';
listeners.create(event_name, hydrateNode, bubbles);
}
}
}
const treeWalker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_COMMENT,
);
let currentNode = treeWalker.nextNode();
while (currentNode) {
const id = currentNode.textContent;
const split = id.split('node-id');
if (split.length > 1) {
nodes[ids[parseInt(split[1])]] = currentNode.nextSibling;
}
currentNode = treeWalker.nextNode();
}
}
export function get_node(id) {
return nodes[id];
}
export function initialize(root, handler) {
listeners.handler = handler;
nodes = [root];
stack = [root];
listeners.root = root;
}
function AppendChildren(id, many){
root = nodes[id];
els = stack.splice(stack.length-many);
for (k = 0; k < many; k++) {
root.appendChild(els[k]);
}
}
"#;
extern "C" {
#[wasm_bindgen]
pub fn save_template(nodes: Vec<Node>, tmpl_id: u16);
#[wasm_bindgen]
pub fn hydrate(ids: Vec<u32>);
#[wasm_bindgen]
pub fn get_node(id: u32) -> Node;
#[wasm_bindgen]
pub fn initialize(root: Node, handler: &Function);
}
fn mount_to_root() {
"{AppendChildren(root, stack.length-1);}"
}
fn push_root(root: u32) {
"{stack.push(nodes[$root$]);}"
}
fn append_children(id: u32, many: u16) {
"{AppendChildren($id$, $many$);}"
}
fn pop_root() {
"{stack.pop();}"
}
fn replace_with(id: u32, n: u16) {
"{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
}
fn insert_after(id: u32, n: u16) {
"{nodes[$id$].after(...stack.splice(stack.length-$n$));}"
}
fn insert_before(id: u32, n: u16) {
"{nodes[$id$].before(...stack.splice(stack.length-$n$));}"
}
fn remove(id: u32) {
"{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}"
}
fn create_raw_text(text: &str) {
"{stack.push(document.createTextNode($text$));}"
}
fn create_text_node(text: &str, id: u32) {
"{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}"
}
fn create_placeholder(id: u32) {
"{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}"
}
fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
r#"node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); listeners.create($event_name$, node, $bubbles$);"#
}
fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
"{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}"
}
fn set_text(id: u32, text: &str) {
"{nodes[$id$].textContent = $text$;}"
}
fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
"{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}"
}
fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
r#"{
node = nodes[$id$];
if (!ns) {
switch (field) {
case "value":
node.value = "";
break;
case "checked":
node.checked = false;
break;
case "selected":
node.selected = false;
break;
case "dangerous_inner_html":
node.innerHTML = "";
break;
default:
node.removeAttribute(field);
break;
}
} else if (ns == "style") {
node.style.removeProperty(name);
} else {
node.removeAttributeNS(ns, field);
}
}"#
}
fn assign_id(ptr: u32, len: u8, id: u32) {
"{nodes[$id$] = LoadChild($ptr$, $len$);}"
}
fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) {
r#"{
node = LoadChild($ptr$, $len$);
if (node.nodeType == Node.TEXT_NODE) {
node.textContent = value;
} else {
let text = document.createTextNode(value);
node.replaceWith(text);
node = text;
}
nodes[$id$] = node;
}"#
}
fn replace_placeholder(ptr: u32, len: u8, n: u16) {
"{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}"
}
fn load_template(tmpl_id: u16, index: u16, id: u32) {
"{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}"
}
}
#[cfg(feature = "binary-protocol")]
pub mod binary_protocol {
use sledgehammer_bindgen::bindgen;
pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
#[bindgen]
mod protocol_js {
const JS_FILE: &str = "./src/interpreter.js";
const JS_FILE: &str = "./src/common.js";
fn mount_to_root() {
"{AppendChildren(root, stack.length-1);}"
}
fn push_root(root: u32) {
"{stack.push(nodes[$root$]);}"
}
fn append_children(id: u32, many: u16) {
"{AppendChildren($id$, $many$);}"
}
fn append_children_to_top(many: u16) {
"{
root = stack[stack.length-many-1];
els = stack.splice(stack.length-many);
for (k = 0; k < many; k++) {
root.appendChild(els[k]);
}
}"
}
fn pop_root() {
"{stack.pop();}"
}
fn replace_with(id: u32, n: u16) {
"{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
}
fn insert_after(id: u32, n: u16) {
"{nodes[$id$].after(...stack.splice(stack.length-$n$));}"
}
fn insert_before(id: u32, n: u16) {
"{nodes[$id$].before(...stack.splice(stack.length-$n$));}"
}
fn remove(id: u32) {
"{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}"
}
fn create_raw_text(text: &str) {
"{stack.push(document.createTextNode($text$));}"
}
fn create_text_node(text: &str, id: u32) {
"{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}"
}
fn create_element(element: &'static str<u8, el>) {
"{stack.push(document.createElement($element$))}"
}
fn create_element_ns(element: &'static str<u8, el>, ns: &'static str<u8, namespace>) {
"{stack.push(document.createElementNS($ns$, $element$))}"
}
fn create_placeholder(id: u32) {
"{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}"
}
fn add_placeholder() {
"{node = document.createElement('pre'); node.hidden = true; stack.push(node);}"
}
fn new_event_listener(event: &str<u8, evt>, id: u32, bubbles: u8) {
r#"
bubbles = bubbles == 1;
node = nodes[id];
if(node.listening){
node.listening += 1;
} else {
node.listening = 1;
}
node.setAttribute('data-dioxus-id', `\${id}`);
const event_name = $event$;
// if this is a mounted listener, we send the event immediately
if (event_name === "mounted") {
window.ipc.postMessage(
window.interpreter.serializeIpcMessage("user_event", {
name: event_name,
element: id,
data: null,
bubbles,
})
);
} else {
listeners.create(event_name, node, bubbles, (event) => {
handler(event, event_name, bubbles, config);
});
}"#
}
fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
"{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}"
}
fn set_text(id: u32, text: &str) {
"{nodes[$id$].textContent = $text$;}"
}
fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
"{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}"
}
fn set_top_attribute(field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
"{setAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}"
}
fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
r#"{
node = nodes[$id$];
if (!ns) {
switch (field) {
case "value":
node.value = "";
break;
case "checked":
node.checked = false;
break;
case "selected":
node.selected = false;
break;
case "dangerous_inner_html":
node.innerHTML = "";
break;
default:
node.removeAttribute(field);
break;
}
} else if (ns == "style") {
node.style.removeProperty(name);
} else {
node.removeAttributeNS(ns, field);
}
}"#
}
fn assign_id(array: &[u8], id: u32) {
"{nodes[$id$] = LoadChild($array$);}"
}
fn hydrate_text(array: &[u8], value: &str, id: u32) {
r#"{
node = LoadChild($array$);
if (node.nodeType == Node.TEXT_NODE) {
node.textContent = value;
} else {
let text = document.createTextNode(value);
node.replaceWith(text);
node = text;
}
nodes[$id$] = node;
}"#
}
fn replace_placeholder(array: &[u8], n: u16) {
"{els = stack.splice(stack.length - $n$); node = LoadChild($array$); node.replaceWith(...els);}"
}
fn load_template(tmpl_id: u16, index: u16, id: u32) {
"{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}"
}
fn add_templates(tmpl_id: u16, len: u16) {
"{templates[$tmpl_id$] = stack.splice(stack.length-$len$);}"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
function retrieveFormValues() {
}
@@ -0,0 +1,82 @@
// The root interpreter class that holds state about the mapping between DOM and VirtualDom
// This always lives in the JS side of things, and is extended by the native and web interpreters
// todo: we want to make these vars that the interpreter uses to be isolated so we can have multiple interpreters
export let m: DataView; // p, ls, d, t, op, i, e, z, metaflags;
export class Interpreter {
// non bubbling events listen at the element the listener was created at
global: {
[key: string]: { active: number, callback: EventListener }
};
// bubbling events can listen at the root element
local: {
[key: string]: {
[key: string]: EventListener
}
};
root: Element;
handler: EventListener;
nodes: Element[];
stack: Element[];
templates: {
[key: string]: Element[]
};
constructor(root: Element, handler: EventListener) {
this.root = root;
this.nodes = [root];
this.stack = [root];
this.global = {};
this.local = {};
this.handler = handler;
}
createListener(event_name: string, element: Element, bubbles: boolean) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = { active: 1, callback: this.handler };
this.root.addEventListener(event_name, this.handler);
} else {
this.global[event_name].active++;
}
} else {
const id = element.getAttribute("data-dioxus-id");
if (!this.local[id]) {
this.local[id] = {};
}
element.addEventListener(event_name, this.handler);
}
}
removeListener(element: Element, event_name: string, bubbles: boolean) {
if (bubbles) {
this.removeBubblingListener(event_name);
} else {
this.removeNonBubblingListener(element, event_name);
}
}
removeBubblingListener(event_name: string) {
this.global[event_name].active--;
if (this.global[event_name].active === 0) {
this.root.removeEventListener(event_name, this.global[event_name].callback);
delete this.global[event_name];
}
}
removeNonBubblingListener(element: Element, event_name: string) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (Object.keys(this.local[id]).length === 0) {
delete this.local[id];
}
element.removeEventListener(event_name, this.handler);
}
removeAllNonBubblingListeners(element: Element) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id];
}
}
@@ -0,0 +1,286 @@
// This file provides an extended variant of the interpreter used for desktop and liveview interaction
//
//
import { Interpreter } from "./interpreter_core";
export class NativeInterpreter extends Interpreter {
intercept_link_redirects: boolean;
constructor(root: Element) {
super(root, (event) => handler(event, this, event.type, true));
}
serializeIpcMessage(method: string, params = {}) {
return JSON.stringify({ method, params });
}
scrollTo(id: number, behavior: ScrollBehavior) {
const node = this.nodes[id];
if (!(node instanceof HTMLElement)) {
return false;
}
node.scrollIntoView({
behavior: behavior,
});
return true;
}
getClientRect(id: number) {
const node = this.nodes[id];
if (!node) {
return;
}
const rect = node.getBoundingClientRect();
return {
type: "GetClientRect",
origin: [rect.x, rect.y],
size: [rect.width, rect.height],
};
}
setFocus(id: number, focus: boolean) {
const node = this.nodes[id];
if (!(node instanceof HTMLElement)) {
return false;
}
if (focus) {
node.focus();
} else {
node.blur();
}
return true;
}
LoadChild(array: number[]) {
// iterate through each number and get that child
let node = this.stack[this.stack.length - 1] as Node;
for (let i = 0; i < array.length; i++) {
let end = array[i];
for (node = node.firstChild; end > 0; end--) {
node = node.nextSibling;
}
}
return node;
}
AppendChildren(id: number, many: number) {
const root = this.nodes[id];
const els = this.stack.splice(this.stack.length - many);
for (let k = 0; k < many; k++) {
root.appendChild(els[k]);
}
}
}
function handler(event: Event, interpreter: NativeInterpreter, name: string, bubbles: boolean) {
const target = event.target!;
const realId = target_id(target)!;
let contents = serializeEvent(event);
if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) {
const formData = new FormData(target);
for (let name of formData.keys()) {
const fieldType = target.elements[name].type;
switch (fieldType) {
case "select-multiple":
contents.values[name] = formData.getAll(name);
break;
// add cases for fieldTypes that can hold multiple values here
default:
contents.values[name] = formData.get(name);
break;
}
}
}
if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) {
const selectData = target.options;
contents.values["options"] = [];
for (let i = 0; i < selectData.length; i++) {
let option = selectData[i];
if (option.selected) {
contents.values["options"].push(option.value.toString());
}
}
}
// If there's files to read
if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) {
if (target.getAttribute("type") === "file") {
read_files(target, contents, bubbles, realId, name);
return;
}
}
prevents_default(event, target);
// Handle the event on the virtualdom and then process whatever its output was
let body = {
name: name,
data: serializeEvent(event),
element: parseInt(realId),
bubbles,
};
if (waitForVirtualDomPreventDefault(JSON.stringify(body))) {
event.preventDefault();
}
}
export function waitForVirtualDomPreventDefault(contents: string): boolean {
// Handle the event on the virtualdom and then process whatever its output was
const xhr = new XMLHttpRequest();
// Serialize the event and send it to the custom protocol in the Rust side of things
xhr.timeout = 1000;
xhr.open("GET", "/handle/event.please", false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(contents);
// Deserialize the response, and then prevent the default/capture the event if the virtualdom wants to
return JSON.parse(xhr.responseText).preventDefault;
}
async function read_files(target: HTMLInputElement, contents, bubbles, realId, name) {
let files = target.files!;
let file_contents: { [name: string]: number[] } = {};
for (let i = 0; i < files.length; i++) {
const file = files[i];
file_contents[file.name] = Array.from(
new Uint8Array(await file.arrayBuffer())
);
}
contents.files = { files: file_contents };
if (realId === null) {
return;
}
const message = window.interpreter.serializeIpcMessage("user_event", {
name: name,
element: parseInt(realId),
data: contents,
bubbles,
});
window.ipc.postMessage(message);
}
export function target_id(target: EventTarget): string | null {
if (!(target instanceof Element)) {
return null;
}
function find_real_id(target: Element): string | null {
let realId = target.getAttribute(`data-dioxus-id`);
// walk the tree to find the real element
while (realId == null) {
// we've reached the root we don't want to send an event
if (target.parentElement === null) {
return null;
}
target = target.parentElement;
if (target instanceof Element) {
realId = target.getAttribute(`data-dioxus-id`);
}
}
return realId;
}
return find_real_id(target);
}
// This should:
// - prevent form submissions from navigating
// - prevent anchor tags from navigating
// - prevent buttons from submitting forms
// - let the virtualdom attempt to prevent the event
preventDefaults(event: Event, target: EventTarget) {
let preventDefaultRequests: string | null = null;
// Some events can be triggered on text nodes, which don't have attributes
if (target instanceof Element) {
preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`);
}
if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) {
event.preventDefault();
}
if (event.type === "submit") {
event.preventDefault();
}
// Attempt to intercept if the event is a click
if (target instanceof Element && event.type === "click") {
this.preventFormNavigate(event, target);
}
}
preventFormNavigate(event: Event, target: Element) {
// todo call prevent default if it's the right type of event
if (!this.intercept_link_redirects) {
return;
}
// prevent buttons in forms from submitting the form
if (target.tagName === "BUTTON") { // && event.type == "submit"
event.preventDefault();
}
// If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating
let a_element = target.closest("a");
if (a_element == null) {
return;
}
event.preventDefault();
let elementShouldPreventDefault =
preventDefaultRequests && preventDefaultRequests.includes(`onclick`);
let aElementShouldPreventDefault = a_element.getAttribute(
`dioxus-prevent-default`
);
let linkShouldPreventDefault =
aElementShouldPreventDefault &&
aElementShouldPreventDefault.includes(`onclick`);
if (!elementShouldPreventDefault && !linkShouldPreventDefault) {
const href = a_element.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.ipc.postMessage(
window.interpreter.serializeIpcMessage("browser_open", { href })
);
}
}
}
@@ -0,0 +1,83 @@
// The JS<->Rust bridge
// This file is thin, suitable just for the web
// If you want the more full-featured intrepreter, look at the native interpreter which extends this with additional functionality
//
// We're using sledgehammer directly
import { Interpreter, m } from "./interpreter_core";
export class WebInterpreter extends Interpreter {
constructor(root: Element, handler: EventListener) {
super(root, handler);
}
LoadChild(ptr: number, len: number): Node {
// iterate through each number and get that child
let node = this.stack[this.stack.length - 1] as Node;
let ptr_end = ptr + len;
for (; ptr < ptr_end; ptr++) {
let end = m.getUint8(ptr);
for (node = node.firstChild; end > 0; end--) {
node = node.nextSibling;
}
}
return node;
}
saveTemplate(nodes: Element[], tmpl_id: string) {
this.templates[tmpl_id] = nodes;
}
hydrateRoot(ids: { [key: number]: number }) {
const hydrateNodes = document.querySelectorAll('[data-node-hydration]');
for (let i = 0; i < hydrateNodes.length; i++) {
const hydrateNode = hydrateNodes[i];
const hydration = hydrateNode.getAttribute('data-node-hydration');
const split = hydration!.split(',');
const id = ids[parseInt(split[0])];
this.nodes[id] = hydrateNode;
if (split.length > 1) {
// @ts-ignore
hydrateNode.listening = split.length - 1;
hydrateNode.setAttribute('data-dioxus-id', id.toString());
for (let j = 1; j < split.length; j++) {
const listener = split[j];
const split2 = listener.split(':');
const event_name = split2[0];
const bubbles = split2[1] === '1';
this.createListener(event_name, hydrateNode, bubbles);
}
}
}
const treeWalker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_COMMENT,
);
let currentNode = treeWalker.nextNode();
while (currentNode) {
const id = currentNode.textContent!;
const split = id.split('node-id');
if (split.length > 1) {
this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling as Element;
}
currentNode = treeWalker.nextNode();
}
}
getNode(id: number): Element {
return this.nodes[id];
}
appendChildren(id: number, many: number) {
const root = this.nodes[id];
const els = this.stack.splice(this.stack.length - many);
for (let k = 0; k < many; k++) {
this.root.appendChild(els[k]);
}
}
}
+494
View File
@@ -0,0 +1,494 @@
// Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {};
type SerializedEvent = {
values?: { [key: string]: FormDataEntryValue[] } | FormDataEntryValue[];
value?: string;
[key: string]: any;
};
function serializeEvent(event: Event): SerializedEvent {
// copy, cut, paste
if (event instanceof KeyboardEvent) {
return {
char_code: event.charCode,
is_composing: event.isComposing,
key: event.key,
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
key_code: event.keyCode,
shift_key: event.shiftKey,
location: event.location,
repeat: event.repeat,
which: event.which,
code: event.code,
};
}
// "pointerdown" "pointermove" "pointerup" "pointercancel" "gotpointercapture" "lostpointercapture" "pointerenter"
// "pointerleave" "pointerover" "pointerout"
if (event instanceof PointerEvent) {
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,
};
}
if (event instanceof MouseEvent) {
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,
offset_x: event.offsetX,
offset_y: event.offsetY,
page_x: event.pageX,
page_y: event.pageY,
screen_x: event.screenX,
screen_y: event.screenY,
shift_key: event.shiftKey,
};
}
if (event instanceof TouchEvent) {
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,
};
}
if (event instanceof WheelEvent) {
return {
delta_x: event.deltaX,
delta_y: event.deltaY,
delta_z: event.deltaZ,
delta_mode: event.deltaMode,
};
}
if (event instanceof AnimationEvent) {
return {
animation_name: event.animationName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
};
}
if (event instanceof TransitionEvent) {
return {
property_name: event.propertyName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
};
}
if (event instanceof ClipboardEvent) {
return {};
}
if (event instanceof CompositionEvent) {
return {
data: event.data,
};
}
if (event instanceof DragEvent) {
return {
mouse: {
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
shift_key: event.shiftKey,
},
files: [],
};
}
if (event instanceof FocusEvent) {
return {};
}
return {};
}
function isElementNode(node: Node) {
return node.nodeType == 1;
}
function eventBubbles(eventName: string) {
switch (eventName) {
case "copy":
return true;
case "cut":
return true;
case "paste":
return true;
case "compositionend":
return true;
case "compositionstart":
return true;
case "compositionupdate":
return true;
case "keydown":
return true;
case "keypress":
return true;
case "keyup":
return true;
case "focus":
return false;
case "focusout":
return true;
case "focusin":
return true;
case "blur":
return false;
case "change":
return true;
case "input":
return true;
case "invalid":
return true;
case "reset":
return true;
case "submit":
return true;
case "click":
return true;
case "contextmenu":
return true;
case "doubleclick":
return true;
case "dblclick":
return true;
case "drag":
return true;
case "dragend":
return true;
case "dragenter":
return false;
case "dragexit":
return false;
case "dragleave":
return true;
case "dragover":
return true;
case "dragstart":
return true;
case "drop":
return true;
case "mousedown":
return true;
case "mouseenter":
return false;
case "mouseleave":
return false;
case "mousemove":
return true;
case "mouseout":
return true;
case "scroll":
return false;
case "mouseover":
return true;
case "mouseup":
return true;
case "pointerdown":
return true;
case "pointermove":
return true;
case "pointerup":
return true;
case "pointercancel":
return true;
case "gotpointercapture":
return true;
case "lostpointercapture":
return true;
case "pointerenter":
return false;
case "pointerleave":
return false;
case "pointerover":
return true;
case "pointerout":
return true;
case "select":
return true;
case "touchcancel":
return true;
case "touchend":
return true;
case "touchmove":
return true;
case "touchstart":
return true;
case "wheel":
return true;
case "abort":
return false;
case "canplay":
return false;
case "canplaythrough":
return false;
case "durationchange":
return false;
case "emptied":
return false;
case "encrypted":
return true;
case "ended":
return false;
case "error":
return false;
case "loadeddata":
case "loadedmetadata":
case "loadstart":
case "load":
return false;
case "pause":
return false;
case "play":
return false;
case "playing":
return false;
case "progress":
return false;
case "ratechange":
return false;
case "seeked":
return false;
case "seeking":
return false;
case "stalled":
return false;
case "suspend":
return false;
case "timeupdate":
return false;
case "volumechange":
return false;
case "waiting":
return false;
case "animationstart":
return true;
case "animationend":
return true;
case "animationiteration":
return true;
case "transitionend":
return true;
case "toggle":
return true;
case "mounted":
return false;
}
return true;
}
// switch (event.type) {
// case "copy": case "cut": case "paste": {
// return {};
// }
// case "compositionend":
// case "compositionstart":
// case "compositionupdate": {
// let { data } = event;
// return {
// data,
// };
// }
// case "keydown": case "keypress": case "keyup": {
// if (event instanceof KeyboardEvent) {
// return {
// char_code: event.charCode,
// is_composing: event.isComposing,
// key: event.key,
// alt_key: event.altKey,
// ctrl_key: event.ctrlKey,
// meta_key: event.metaKey,
// key_code: event.keyCode,
// shift_key: event.shiftKey,
// location: event.location,
// repeat: event.repeat,
// which: event.which,
// code: event.code,
// };
// };
// }
// case "focus":
// case "blur": {
// return {};
// }
// case "change": {
// let target = event.target;
// let value;
// if (target.type === "checkbox" || target.type === "radio") {
// value = target.checked ? "true" : "false";
// } else {
// value = target.value ?? target.textContent;
// }
// return {
// value: value,
// values: {},
// };
// }
// case "input":
// case "invalid":
// case "reset":
// case "submit": {
// let target = event.target;
// let value = target.value ?? target.textContent;
// if (target.type === "checkbox") {
// value = target.checked ? "true" : "false";
// }
// return {
// value: value,
// values: {},
// };
// }
// case "drag":
// case "dragend":
// case "dragenter":
// case "dragexit":
// case "dragleave":
// case "dragover":
// case "dragstart":
// case "drop": {
// let files = [];
// if (event.dataTransfer && event.dataTransfer.files) {
// files = ["a", "b", "c"];
// // files = await serializeFileList(event.dataTransfer.files);
// }
// return { mouse: get_mouse_data(event), files };
// }
// case "click":
// case "contextmenu":
// case "doubleclick":
// case "dblclick":
// case "mousedown":
// case "mouseenter":
// case "mouseleave":
// case "mousemove":
// case "mouseout":
// case "mouseover":
// case "mouseup": {
// return get_mouse_data(event);
// }
// case "select": {
// return {};
// }
// case "touchcancel":
// case "touchend":
// case "touchmove":
// case "touchstart": {
// const { altKey, ctrlKey, metaKey, shiftKey } = event;
// return {
// // changed_touches: event.changedTouches,
// // target_touches: event.targetTouches,
// // touches: event.touches,
// alt_key: altKey,
// ctrl_key: ctrlKey,
// meta_key: metaKey,
// shift_key: shiftKey,
// };
// }
// case "scroll": {
// return {};
// }
// case "wheel": {
// const { deltaX, deltaY, deltaZ, deltaMode } = event;
// return {
// delta_x: deltaX,
// delta_y: deltaY,
// delta_z: deltaZ,
// delta_mode: deltaMode,
// };
// }
// case "animationstart":
// case "animationend":
// case "animationiteration": {
// const { animationName, elapsedTime, pseudoElement } = event;
// return {
// animation_name: animationName,
// elapsed_time: elapsedTime,
// pseudo_element: pseudoElement,
// };
// }
// case "transitionend": {
// const { propertyName, elapsedTime, pseudoElement } = event;
// return {
// property_name: propertyName,
// elapsed_time: elapsedTime,
// pseudo_element: pseudoElement,
// };
// }
// case "abort":
// case "canplay":
// case "canplaythrough":
// case "durationchange":
// case "emptied":
// case "encrypted":
// case "ended":
// case "error":
// case "loadeddata":
// case "loadedmetadata":
// case "loadstart":
// case "pause":
// case "play":
// case "playing":
// case "progress":
// case "ratechange":
// case "seeked":
// case "seeking":
// case "stalled":
// case "suspend":
// case "timeupdate":
// case "volumechange":
// case "waiting": {
// return {};
// }
// case "toggle": {
// return {};
// }
// default: {
// return {};
// }
// }
@@ -0,0 +1,105 @@
// this function should try and stay fast, if possible
export function setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) {
// we support a single namespace by default: style
if (ns === "style") {
node.style.setProperty(field, value);
return;
}
// If there's a namespace, use setAttributeNS (svg, mathml, etc.)
if (!!ns) {
node.setAttributeNS(ns, field, value);
return
}
// A few attributes are need to be set with either boolean values or require some sort of translation
switch (field) {
case "value":
if (value !== node.getAttribute("value")) {
node.setAttribute(field, value);
}
break;
case "initial_value":
node.setAttribute("defaultValue", value);
break;
case "checked":
if (node instanceof HTMLInputElement) {
node.checked = truthy(value);
}
break;
case "initial_checked":
if (node instanceof HTMLInputElement) {
node.defaultChecked = truthy(value);
}
break;
case "selected":
if (node instanceof HTMLOptionElement) {
node.selected = truthy(value);
}
break;
case "initial_selected":
if (node instanceof HTMLOptionElement) {
node.defaultSelected = truthy(value);
}
break;
case "dangerous_inner_html":
node.innerHTML = value;
break;
// The presence of a an attribute is enough to set it to true, provided the value is being set to a truthy value
// Again, kinda ugly and would prefer this logic to be baked into dioxus-html at compiile time
default:
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
if (!truthy(value) && isBoolAttr(field)) {
node.removeAttribute(field);
} else {
node.setAttribute(field, value);
}
}
}
function truthy(val: string | boolean) {
return val === "true" || val === true;
}
function isBoolAttr(field: string): boolean {
switch (field) {
case "allowfullscreen":
case "allowpaymentrequest":
case "async":
case "autofocus":
case "autoplay":
case "checked":
case "controls":
case "default":
case "defer":
case "disabled":
case "formnovalidate":
case "hidden":
case "ismap":
case "itemscope":
case "loop":
case "multiple":
case "muted":
case "nomodule":
case "novalidate":
case "open":
case "playsinline":
case "readonly":
case "required":
case "reversed":
case "selected":
case "truespeed":
case "webkitdirectory":
return true;
default:
return false;
}
}
@@ -0,0 +1,196 @@
#[cfg(feature = "webonly")]
use js_sys::Function;
#[cfg(feature = "webonly")]
use web_sys::Node;
use sledgehammer_bindgen::bindgen;
pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
#[bindgen(module)]
mod js {
// Load in the JavaScript file with all the imports
const JS_FILE: &str = "./src/gen/interpreter.js";
// Boot the interpreter and attach us some bindings!
const JS: &str = r#""#;
fn mount_to_root() {
"{AppendChildren(root, stack.length-1);}"
}
fn push_root(root: u32) {
"{stack.push(nodes[$root$]);}"
}
fn append_children(id: u32, many: u16) {
"{AppendChildren($id$, $many$);}"
}
fn pop_root() {
"{stack.pop();}"
}
fn replace_with(id: u32, n: u16) {
"{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
}
fn insert_after(id: u32, n: u16) {
"{nodes[$id$].after(...stack.splice(stack.length-$n$));}"
}
fn insert_before(id: u32, n: u16) {
"{nodes[$id$].before(...stack.splice(stack.length-$n$));}"
}
fn remove(id: u32) {
"{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}"
}
fn create_raw_text(text: &str) {
"{stack.push(document.createTextNode($text$));}"
}
fn create_text_node(text: &str, id: u32) {
"{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}"
}
fn create_placeholder(id: u32) {
"{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}"
}
fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
r#"node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); listeners.create($event_name$, node, $bubbles$);"#
}
fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
"{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}"
}
fn set_text(id: u32, text: &str) {
"{nodes[$id$].textContent = $text$;}"
}
fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
"{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}"
}
fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
r#"{
node = nodes[$id$];
if (!ns) {
switch (field) {
case "value":
node.value = "";
break;
case "checked":
node.checked = false;
break;
case "selected":
node.selected = false;
break;
case "dangerous_inner_html":
node.innerHTML = "";
break;
default:
node.removeAttribute(field);
break;
}
} else if (ns == "style") {
node.style.removeProperty(name);
} else {
node.removeAttributeNS(ns, field);
}
}"#
}
fn assign_id(ptr: u32, len: u8, id: u32) {
"{nodes[$id$] = LoadChild($ptr$, $len$);}"
}
fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) {
r#"{
node = LoadChild($ptr$, $len$);
if (node.nodeType == Node.TEXT_NODE) {
node.textContent = value;
} else {
let text = document.createTextNode(value);
node.replaceWith(text);
node = text;
}
nodes[$id$] = node;
}"#
}
fn replace_placeholder(ptr: u32, len: u8, n: u16) {
"{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}"
}
fn load_template(tmpl_id: u16, index: u16, id: u32) {
"{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}"
}
/*
Binary protocol methods only!
These methods let us support binary packing mutations for use on boundaries like desktop where we prefer to send
binary data instead of JSON.
We're using native types in a number of places
*/
fn append_children_to_top(many: u16) {
"{
root = stack[stack.length-many-1];
els = stack.splice(stack.length-many);
for (k = 0; k < many; k++) {
root.appendChild(els[k]);
}
}"
}
fn set_top_attribute(field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
"{setAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}"
}
fn add_placeholder() {
"{node = document.createElement('pre'); node.hidden = true; stack.push(node);}"
}
fn create_element_ns(element: &'static str<u8, el>, ns: &'static str<u8, namespace>) {
"{stack.push(document.createElementNS($ns$, $element$))}"
}
fn create_element(element: &'static str<u8, el>) {
"{stack.push(document.createElement($element$))}"
}
fn add_templates(tmpl_id: u16, len: u16) {
"{templates[$tmpl_id$] = stack.splice(stack.length-$len$);}"
}
fn foreign_event_listener(event: &str<u8, evt>, id: u32, bubbles: u8) {
r#"
bubbles = bubbles == 1;
node = nodes[id];
if(node.listening){
node.listening += 1;
} else {
node.listening = 1;
}
node.setAttribute('data-dioxus-id', `\${id}`);
const event_name = $event$;
// if this is a mounted listener, we send the event immediately
if (event_name === "mounted") {
window.ipc.postMessage(
window.interpreter.serializeIpcMessage("user_event", {
name: event_name,
element: id,
data: null,
bubbles,
})
);
} else {
listeners.create(event_name, node, bubbles, (event) => {
handler(event, event_name, bubbles, config);
});
}"#
}
fn assign_id_ref(array: &[u8], id: u32) {
"{nodes[$id$] = LoadChild($array$);}"
}
fn hydrate_text_ref(array: &[u8], value: &str, id: u32) {
r#"{
node = LoadChild($array$);
if (node.nodeType == Node.TEXT_NODE) {
node.textContent = value;
} else {
let text = document.createTextNode(value);
node.replaceWith(text);
node = text;
}
nodes[$id$] = node;
}"#
}
fn replace_placeholder_ref(array: &[u8], n: u16) {
"{els = stack.splice(stack.length - $n$); node = LoadChild($array$); node.replaceWith(...els);}"
}
}
@@ -1,17 +1,18 @@
use dioxus_html::event_bubbles;
// use crate::binary_protocol::Channel;
use crate::unified_bindings::Channel;
use dioxus_core::{TemplateAttribute, TemplateNode, WriteMutations};
use dioxus_html::event_bubbles;
use sledgehammer_utils::rustc_hash::FxHashMap;
use crate::binary_protocol::Channel;
/// The state needed to apply mutations to a channel. This state should be kept across all mutations for the app
#[derive(Default)]
pub struct MutationState {
/// The maximum number of templates that we have registered
max_template_count: u16,
/// The currently registered templates with the template ids
templates: FxHashMap<String, u16>,
/// The channel that we are applying mutations to
channel: Channel,
}
@@ -100,7 +101,7 @@ impl WriteMutations for MutationState {
}
fn assign_node_id(&mut self, path: &'static [u8], id: dioxus_core::ElementId) {
self.channel.assign_id(path, id.0 as u32);
self.channel.assign_id_ref(path, id.0 as u32);
}
fn create_placeholder(&mut self, id: dioxus_core::ElementId) {
@@ -112,7 +113,7 @@ impl WriteMutations for MutationState {
}
fn hydrate_text_node(&mut self, path: &'static [u8], value: &str, id: dioxus_core::ElementId) {
self.channel.hydrate_text(path, value, id.0 as u32);
self.channel.hydrate_text_ref(path, value, id.0 as u32);
}
fn load_template(&mut self, name: &'static str, index: usize, id: dioxus_core::ElementId) {
@@ -127,7 +128,7 @@ impl WriteMutations for MutationState {
}
fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) {
self.channel.replace_placeholder(path, m as u16);
self.channel.replace_placeholder_ref(path, m as u16);
}
fn insert_nodes_after(&mut self, id: dioxus_core::ElementId, m: usize) {
@@ -181,8 +182,9 @@ impl WriteMutations for MutationState {
}
fn create_event_listener(&mut self, name: &'static str, id: dioxus_core::ElementId) {
// note that we use the foreign event listener here
self.channel
.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
.foreign_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}
fn remove_event_listener(&mut self, name: &'static str, id: dioxus_core::ElementId) {