Add proper bundling to tsc

This commit is contained in:
Jonathan Kelley committed 2024-02-27 16:33:18 -08:00
1 parent bdacca0dfb
commit 4b64894fc5
15 files changed
+267 -1065

No files matched your search

+2 -2
View File
@@ -23,7 +23,7 @@ fn main() {
fn gen_bindings(name: &str) {
let contents = read_to_string(&format!("src/{name}.ts")).unwrap();
let generated = read_to_string(&format!("src/gen/{name}.js")).unwrap_or_default();
let hashed = hash_file(&contents);
let hashed = hash_it(&contents);
// If the file is generated, and the hash is the same, we're good, don't do anything
if generated
@@ -58,7 +58,7 @@ fn gen_bindings(name: &str) {
std::fs::write(&format!("src/gen/{name}.js"), generated).unwrap();
}
fn hash_file(obj: &str) -> u64 {
fn hash_it(obj: impl Hash) -> u64 {
let mut hasher = DefaultHasher::new();
obj.hash(&mut hasher);
hasher.finish()
-79
View File
@@ -1,79 +0,0 @@
this.setAttributeInner = function (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;
}
@@ -1,79 +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;
}
-81
View File
@@ -1,81 +0,0 @@
// DO NOT EDIT THIS FILE. HASH: 9578489549991746027
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;
}
-1
View File
@@ -1 +0,0 @@
// DO NOT EDIT THIS FILE. HASH: 3476900567878811119
Whitespace-only changes.
Whitespace-only changes.
-606
View File
@@ -145,609 +145,3 @@ this.handler = async function (event, name, bubbles) {
);
}
}
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];
}
}
this.LoadChild = function (array) {
// iterate through each number and get that child
let node = this.stack[this.stack.length - 1];
for (let i = 0; i < array.length; i++) {
this.end = array[i];
for (node = node.firstChild; this.end > 0; this.end--) {
node = node.nextSibling;
}
}
return node;
}
this.listeners = new ListenerMap();
this.nodes = [];
this.stack = [];
this.root;
this.templates = {};
this.els = null;
this.end = null;
this.AppendChildren = function (id, many) {
this.root = this.nodes[id];
this.els = this.stack.splice(this.stack.length - many);
for (let k = 0; k < many; k++) {
this.root.appendChild(this.els[k]);
}
}
this.initialize = function (root) {
this.nodes = [root];
this.stack = [root];
this.listeners.root = root;
}
this.getClientRect = function (id) {
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],
};
}
this.scrollTo = function (id, behavior) {
const node = this.nodes[id];
if (!node) {
return false;
}
node.scrollIntoView({
behavior: behavior,
});
return true;
}
/// Set the focus on the element
this.setFocus = function (id, focus) {
const node = this.nodes[id];
if (!node) {
return false;
}
if (focus) {
node.focus();
} else {
node.blur();
}
return true;
}
function get_mouse_data(event) {
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 = null;
if (event.dataTransfer && event.dataTransfer.files) {
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 {};
}
}
}
this.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;
}
+51 -1
View File
@@ -1,3 +1,53 @@
function retrieveFormValues() {
// Consistently deserialize forms and form elements for use across web/desktop/mobile
type FormValues = { [key: string]: FormDataEntryValue[] };
export function retriveValues(event: Event, target: HTMLElement): FormValues {
const contents: FormValues = {};
if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) {
retrieveFormValues(target, contents);
}
if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) {
retriveInputsValues(target, contents);
}
return contents;
}
export function retrieveFormValues(form: HTMLFormElement, contents: FormValues) {
const formData = new FormData(form);
for (let name in formData.keys()) {
let element = form.elements.namedItem(name);
// todo: this is going to be a problem for select-multiple?
if (!(element instanceof HTMLInputElement)) {
continue;
}
switch (element.type) {
case "select-multiple":
contents[name] = formData.getAll(name);
break;
// By default, it's just a single value
default:
contents[name] = [formData.get(name)];
break;
}
}
}
export function retriveInputsValues(target: HTMLSelectElement, contents: FormValues,) {
const selectData = target.options;
contents["options"] = [];
for (let i = 0; i < selectData.length; i++) {
let option = selectData[i];
if (option.selected) {
contents["options"].push(option.value.toString());
}
}
}
@@ -1,9 +1,6 @@
// 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: {
@@ -15,15 +12,15 @@ export class Interpreter {
[key: string]: EventListener
}
};
root: Element;
root: HTMLElement;
handler: EventListener;
nodes: Element[];
stack: Element[];
nodes: Node[];
stack: Node[];
templates: {
[key: string]: Element[]
[key: string]: Node[]
};
constructor(root: Element, handler: EventListener) {
constructor(root: HTMLElement, handler: EventListener) {
this.root = root;
this.nodes = [root];
this.stack = [root];
@@ -32,7 +29,7 @@ export class Interpreter {
this.handler = handler;
}
createListener(event_name: string, element: Element, bubbles: boolean) {
createListener(event_name: string, element: HTMLElement, bubbles: boolean) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = { active: 1, callback: this.handler };
@@ -49,7 +46,7 @@ export class Interpreter {
}
}
removeListener(element: Element, event_name: string, bubbles: boolean) {
removeListener(element: HTMLElement, event_name: string, bubbles: boolean) {
if (bubbles) {
this.removeBubblingListener(event_name);
} else {
@@ -65,7 +62,7 @@ export class Interpreter {
}
}
removeNonBubblingListener(element: Element, event_name: string) {
removeNonBubblingListener(element: HTMLElement, event_name: string) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (Object.keys(this.local[id]).length === 0) {
@@ -74,7 +71,7 @@ export class Interpreter {
element.removeEventListener(event_name, this.handler);
}
removeAllNonBubblingListeners(element: Element) {
removeAllNonBubblingListeners(element: HTMLElement) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id];
}
+181 -192
View File
@@ -1,14 +1,27 @@
// This file provides an extended variant of the interpreter used for desktop and liveview interaction
//
//
// This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't
// provide since it doesn't have access to the dom.
import { retriveValues } from "./form";
import { Interpreter } from "./interpreter_core";
import { SerializedEvent, serializeEvent } from "./serialize";
export class NativeInterpreter extends Interpreter {
intercept_link_redirects: boolean;
ipc: any;
constructor(root: Element) {
super(root, (event) => handler(event, this, event.type, true));
// eventually we want to remove liveview and build it into the server-side-events of fullstack
// however, for now we need to support it since SSE in fullstack doesn't exist yet
liveview: boolean;
constructor(root: HTMLElement) {
super(root, (event) => this.handleEvent(event, event.type, true));
this.intercept_link_redirects = true;
this.liveview = false;
// @ts-ignore - wry gives us this
this.ipc = window.ipc;
}
serializeIpcMessage(method: string, params = {}) {
@@ -17,50 +30,38 @@ export class NativeInterpreter extends Interpreter {
scrollTo(id: number, behavior: ScrollBehavior) {
const node = this.nodes[id];
if (!(node instanceof HTMLElement)) {
return false;
if (node instanceof HTMLElement) {
node.scrollIntoView({ behavior });
}
node.scrollIntoView({
behavior: behavior,
});
return true;
}
getClientRect(id: number) {
const node = this.nodes[id];
if (!node) {
return;
if (node instanceof HTMLElement) {
const rect = node.getBoundingClientRect();
return {
type: "GetClientRect",
origin: [rect.x, rect.y],
size: [rect.width, rect.height],
};
}
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 (node instanceof HTMLElement) {
if (focus) {
node.focus();
} else {
node.blur();
}
}
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;
let node = this.stack[this.stack.length - 1];
for (let i = 0; i < array.length; i++) {
let end = array[i];
@@ -80,73 +81,162 @@ export class NativeInterpreter extends Interpreter {
root.appendChild(els[k]);
}
}
}
handleEvent(event: Event, name: string, bubbles: boolean) {
const target = event.target!;
const realId = targetId(target)!;
const contents = serializeEvent(event);
// Attempt to retrive the values from the form and inputs
if (target instanceof HTMLElement) {
contents.values = retriveValues(event, target);
}
// Handle the event on the virtualdom and then preventDefault if it also preventsDefault
// Some listeners
let body = {
name: name,
data: contents,
element: realId,
bubbles,
};
// Run any prevent defaults the user might've set
// This is to support the prevent_default: "onclick" attribute that dioxus has had for a while, but is not necessary
// now that we expose preventDefault to the virtualdom on desktop
// Liveview will still need to use this
this.preventDefaults(event, target);
// liveview does not have syncronous event handling, so we need to send the event to the host
if (this.liveview) {
// Okay, so the user might've requested some files to be read
if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) {
if (target.getAttribute("type") === "file") {
this.readFiles(target, contents, bubbles, realId, name);
}
}
} else {
// Run the event handler on the virtualdom
// capture/prevent default of the event if the virtualdom wants to
const res = handleVirtualdomEventSync(JSON.stringify(body));
function handler(event: Event, interpreter: NativeInterpreter, name: string, bubbles: boolean) {
const target = event.target!;
const realId = target_id(target)!;
let contents = serializeEvent(event);
if (res.preventDefault) {
event.preventDefault();
}
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 (res.stopPropagation) {
event.stopPropagation();
}
}
}
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());
}
async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: number, name: string) {
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 };
const message = this.serializeIpcMessage("user_event", {
name: name,
element: realId,
data: contents,
bubbles,
});
this.ipc.postMessage(message);
}
// 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.handleClickNavigate(event, target, preventDefaultRequests);
}
}
// 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);
handleClickNavigate(event: Event, target: Element, preventDefaultRequests: string) {
// todo call prevent default if it's the right type of event
if (!this.intercept_link_redirects) {
return;
}
}
prevents_default(event, target);
// 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;
}
// 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();
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) {
this.ipc.postMessage(
this.serializeIpcMessage("browser_open", { href })
);
}
}
}
}
export function waitForVirtualDomPreventDefault(contents: string): boolean {
type EventSyncResult = {
preventDefault: boolean;
stopPropagation: boolean;
stopImmediatePropagation: boolean;
filesRequested: boolean;
};
// This function sends the event to the virtualdom and then waits for the virtualdom to process it
//
// However, it's not really suitable for liveview, because it's synchronous and will block the main thread
// We should definitely consider using a websocket if we want to block... or just not block on liveview
// Liveview is a little bit of a tricky beast
function handleVirtualdomEventSync(contents: string): EventSyncResult {
// Handle the event on the virtualdom and then process whatever its output was
const xhr = new XMLHttpRequest();
@@ -157,130 +247,29 @@ export function waitForVirtualDomPreventDefault(contents: string): boolean {
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;
return JSON.parse(xhr.responseText);
}
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)) {
function targetId(target: EventTarget): number | null {
// Ensure that the target is a node, sometimes it's nota
if (!(target instanceof Node)) {
return null;
}
function find_real_id(target: Element): string | null {
let realId = target.getAttribute(`data-dioxus-id`);
let ourTarget = target;
let realId = null;
// 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`);
}
while (realId == null) {
if (ourTarget === null) {
return null;
}
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 })
);
if (ourTarget instanceof Element) {
realId = ourTarget.getAttribute(`data-dioxus-id`);
}
ourTarget = ourTarget.parentNode;
}
return parseInt(realId);
}
+15 -8
View File
@@ -4,10 +4,12 @@
//
// We're using sledgehammer directly
import { Interpreter, m } from "./interpreter_core";
import { Interpreter } from "./interpreter_core";
export class WebInterpreter extends Interpreter {
constructor(root: Element, handler: EventListener) {
m: any;
constructor(root: HTMLElement, handler: EventListener) {
super(root, handler);
}
@@ -17,7 +19,7 @@ export class WebInterpreter extends Interpreter {
let ptr_end = ptr + len;
for (; ptr < ptr_end; ptr++) {
let end = m.getUint8(ptr);
let end = this.m.getUint8(ptr);
for (node = node.firstChild; end > 0; end--) {
node = node.nextSibling;
}
@@ -26,7 +28,7 @@ export class WebInterpreter extends Interpreter {
return node;
}
saveTemplate(nodes: Element[], tmpl_id: string) {
saveTemplate(nodes: HTMLElement[], tmpl_id: string) {
this.templates[tmpl_id] = nodes;
}
@@ -34,7 +36,7 @@ export class WebInterpreter extends Interpreter {
const hydrateNodes = document.querySelectorAll('[data-node-hydration]');
for (let i = 0; i < hydrateNodes.length; i++) {
const hydrateNode = hydrateNodes[i];
const hydrateNode = hydrateNodes[i] as HTMLElement;
const hydration = hydrateNode.getAttribute('data-node-hydration');
const split = hydration!.split(',');
const id = ids[parseInt(split[0])];
@@ -54,22 +56,27 @@ export class WebInterpreter extends Interpreter {
}
}
}
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;
this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling;
}
currentNode = treeWalker.nextNode();
}
}
getNode(id: number): Element {
getNode(id: number): Node {
return this.nodes[id];
}
@@ -77,7 +84,7 @@ export class WebInterpreter extends Interpreter {
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]);
root.appendChild(els[k]);
}
}
}
+3 -3
View File
@@ -1,12 +1,12 @@
// Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {};
type SerializedEvent = {
values?: { [key: string]: FormDataEntryValue[] } | FormDataEntryValue[];
export type SerializedEvent = {
values?: { [key: string]: FormDataEntryValue[] };
value?: string;
[key: string]: any;
};
function serializeEvent(event: Event): SerializedEvent {
export function serializeEvent(event: Event): SerializedEvent {
// copy, cut, paste
if (event instanceof KeyboardEvent) {
@@ -1,3 +1,5 @@
// A unified interface for setting attributes on a node
// 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
+4 -1
View File
@@ -3,7 +3,10 @@
"module": "system",
"lib": [
"ES2015",
"DOM"
"DOM",
"dom",
"dom.iterable",
"esnext"
],
"noImplicitAny": true,
"removeComments": true,