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

+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