fix navigating when files are dropped

This commit is contained in:
Jonathan Kelley committed 2024-03-01 23:37:46 -08:00
1 parent 0ff0eb7846
commit 199173a409
23 files changed
+1242 -1026

No files matched your search

+41 -36
View File
@@ -1,53 +1,58 @@
// Consistently deserialize forms and form elements for use across web/desktop/mobile
type FormValues = { [key: string]: FormDataEntryValue[] };
type FormValues = {
valid?: boolean;
values: { [key: string]: FormDataEntryValue };
}
export function retriveValues(event: Event, target: HTMLElement): FormValues {
const contents: FormValues = {};
let contents: FormValues = {
values: {}
};
if (target instanceof HTMLFormElement && (event.type === "submit" || event.type === "input")) {
retrieveFormValues(target, contents);
}
// If there's a form...
let form = target.closest("form");
if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) {
retriveInputsValues(target, contents);
// If the target is an input, and the event is input or change, we want to get the value without going through the form
if (form) {
if (
event.type === "input"
|| event.type === "change"
|| event.type === "submit"
|| event.type === "reset"
|| event.type === "click"
) {
contents = retrieveFormValues(form);
}
}
return contents;
}
export function retrieveFormValues(form: HTMLFormElement, contents: FormValues) {
// todo: maybe encode spaces or something?
// We encode select multiple as a comma separated list which breaks... when there's commas in the values
export function retrieveFormValues(form: HTMLFormElement): 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;
const contents: { [key: string]: FormDataEntryValue } = {};
formData.forEach((value, key) => {
if (contents[key]) {
contents[key] += "," + value;
} else {
contents[key] = value;
}
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;
}
}
});
return {
valid: form.checkValidity(),
values: contents
};
}
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());
}
export function retriveSelectValue(target: HTMLSelectElement): string[] {
// there might be multiple...
let options = target.selectedOptions;
let values = [];
for (let i = 0; i < options.length; i++) {
values.push(options[i].value);
}
return values;
}
@@ -1,6 +1,8 @@
// 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
export type NodeId = number;
export class Interpreter {
// non bubbling events listen at the element the listener was created at
global: {
@@ -22,12 +24,18 @@ export class Interpreter {
};
constructor(root: HTMLElement, handler: EventListener) {
this.root = root;
this.nodes = [root];
this.stack = [root];
this.handler = handler;
this.initialize(root);
}
initialize(root: HTMLElement) {
this.global = {};
this.local = {};
this.handler = handler;
this.root = root;
this.nodes = [root];
this.stack = [root];
this.templates = {};
}
createListener(event_name: string, element: HTMLElement, bubbles: boolean) {
@@ -77,11 +85,11 @@ export class Interpreter {
delete this.local[id];
}
getNode(id: number): Node {
getNode(id: NodeId): Node {
return this.nodes[id];
}
appendChildren(id: number, many: number) {
appendChildren(id: NodeId, many: number) {
const root = this.nodes[id];
const els = this.stack.splice(this.stack.length - many);
for (let k = 0; k < many; k++) {
@@ -3,12 +3,11 @@
// 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 { Interpreter, NodeId } from "./interpreter_core";
import { SerializedEvent, serializeEvent } from "./serialize";
import { setAttributeInner } from "./set_attribute";
export class NativeInterpreter extends Interpreter {
export class PlatformInterpreter extends Interpreter {
intercept_link_redirects: boolean;
ipc: any;
@@ -21,6 +20,23 @@ export class NativeInterpreter extends Interpreter {
this.intercept_link_redirects = true;
this.liveview = false;
// attach an event listener on the body that prevents file drops from navigating
// this is because the browser will try to navigate to the file if it's dropped on the window
window.addEventListener("dragover", function (e) {
// check which element is our target
if (e.target instanceof Element && e.target.tagName != "INPUT") {
e.preventDefault();
}
}, false);
window.addEventListener("drop", function (e) {
// check which element is our target
if (e.target instanceof Element && e.target.tagName != "INPUT") {
e.preventDefault();
}
}, false);
// @ts-ignore - wry gives us this
this.ipc = window.ipc;
}
@@ -29,14 +45,18 @@ export class NativeInterpreter extends Interpreter {
return JSON.stringify({ method, params });
}
scrollTo(id: number, behavior: ScrollBehavior) {
setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) {
setAttributeInner(node, field, value, ns);
}
scrollTo(id: NodeId, behavior: ScrollBehavior) {
const node = this.nodes[id];
if (node instanceof HTMLElement) {
node.scrollIntoView({ behavior });
}
}
getClientRect(id: number) {
getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined {
const node = this.nodes[id];
if (node instanceof HTMLElement) {
const rect = node.getBoundingClientRect();
@@ -48,7 +68,7 @@ export class NativeInterpreter extends Interpreter {
}
}
setFocus(id: number, focus: boolean) {
setFocus(id: NodeId, focus: boolean) {
const node = this.nodes[id];
if (node instanceof HTMLElement) {
@@ -74,7 +94,7 @@ export class NativeInterpreter extends Interpreter {
return node;
}
AppendChildren(id: number, many: number) {
AppendChildren(id: NodeId, many: number) {
const root = this.nodes[id];
const els = this.stack.splice(this.stack.length - many);
@@ -86,12 +106,7 @@ export class NativeInterpreter extends Interpreter {
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);
}
const contents = serializeEvent(event, target);
// Handle the event on the virtualdom and then preventDefault if it also preventsDefault
// Some listeners
@@ -119,21 +134,26 @@ export class NativeInterpreter extends Interpreter {
}
} 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));
const message = this.serializeIpcMessage("user_event", body);
this.ipc.postMessage(message);
if (res.preventDefault) {
event.preventDefault();
}
// // Run the event handler on the virtualdom
// // capture/prevent default of the event if the virtualdom wants to
// const res = handleVirtualdomEventSync(JSON.stringify(body));
if (res.stopPropagation) {
event.stopPropagation();
}
// if (res.preventDefault) {
// event.preventDefault();
// }
// if (res.stopPropagation) {
// event.stopPropagation();
// }
}
}
async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: number, name: string) {
// A liveview only function
// Desktop will intercept the event before it hits this
async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) {
let files = target.files!;
let file_contents: { [name: string]: number[] } = {};
@@ -251,7 +271,7 @@ function handleVirtualdomEventSync(contents: string): EventSyncResult {
return JSON.parse(xhr.responseText);
}
function targetId(target: EventTarget): number | null {
function targetId(target: EventTarget): NodeId | null {
// Ensure that the target is a node, sometimes it's nota
if (!(target instanceof Node)) {
return null;
@@ -6,7 +6,7 @@
import { Interpreter } from "./interpreter_core";
export class WebInterpreter extends Interpreter {
export class PlatformInterpreter extends Interpreter {
m: any;
constructor(root: HTMLElement, handler: EventListener) {
+202 -144
View File
@@ -1,159 +1,217 @@
// Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {};
import { retriveSelectValue, retriveValues } from "./form";
export type AppTouchEvent = TouchEvent;
export type SerializedEvent = {
values?: { [key: string]: FormDataEntryValue[] };
values?: { [key: string]: FormDataEntryValue };
value?: string;
[key: string]: any;
};
export function serializeEvent(event: Event): SerializedEvent {
if (event instanceof InputEvent) {
if (event.target instanceof HTMLInputElement) {
let target = event.target;
let value = target.value ?? target.textContent;
if (target.type === "checkbox") {
value = target.checked ? "true" : "false";
}
return {
value: value,
values: {},
};
}
return {};
}
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,
};
}
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 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 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,
};
export function serializeEvent(event: Event, target: EventTarget): SerializedEvent {
let contents = {};
// merge the object into the contents
let extend = (obj: any) => (contents = { ...contents, ...obj });
if (event instanceof WheelEvent) { extend(serializeWheelEvent(event)) };
if (event instanceof MouseEvent) { extend(serializeMouseEvent(event)) }
if (event instanceof KeyboardEvent) { extend(serializeKeyboardEvent(event)) }
if (event instanceof InputEvent) { extend(serializeInputEvent(event, target)) }
if (event instanceof PointerEvent) { extend(serializePointerEvent(event)) }
if (event instanceof AnimationEvent) { extend(serializeAnimationEvent(event)) }
if (event instanceof TransitionEvent) { extend({ property_name: event.propertyName, elapsed_time: event.elapsedTime, pseudo_element: event.pseudoElement, }) }
if (event instanceof CompositionEvent) { extend({ data: event.data, }) }
if (event instanceof DragEvent) { extend(serializeDragEvent(event)) }
if (event instanceof FocusEvent) { extend({}) }
if (event instanceof ClipboardEvent) { extend({}) }
// safari is quirky and doesn't have TouchEvent
if (typeof TouchEvent !== 'undefined' && event instanceof TouchEvent) { extend(serializeTouchEvent(event)); }
if (event.type === "submit" || event.type === "reset" || event.type === "click" || event.type === "change" || event.type === "input") {
extend(serializeInputEvent(event as InputEvent, target));
}
// If there's any files, we need to serialize them
if (event instanceof DragEvent) {
// 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 };
return {
mouse: {
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
shift_key: event.shiftKey,
},
files: [],
};
// let files: { [key: string]: Uint8Array } = {};
// if (event.dataTransfer && event.dataTransfer.files) {
// files["a"] = new Uint8Array(0);
// // files = {
// // entries: Array.from(event.dataTransfer.files).map((file) => {
// // return {
// // name: file.name,
// // type: file.type,
// // size: file.size,
// // last_modified: file.lastModified,
// // };
// // }
// // };
// // files = await serializeFileList(event.dataTransfer.files);
// }
// extend({ files: files });
}
if (event instanceof FocusEvent) {
return {};
return contents;
}
function serializeInputEvent(event: InputEvent, target: EventTarget): SerializedEvent {
let contents: SerializedEvent = {};
// Attempt to retrieve the values from the form
if (target instanceof HTMLElement) {
let values = retriveValues(event, target);
contents.values = values.values;
contents.valid = values.valid;
}
if (event.target instanceof HTMLInputElement) {
let target = event.target;
let value = target.value ?? target.textContent ?? "";
if (target.type === "checkbox") {
value = target.checked ? "true" : "false";
} else if (target.type === "radio") {
value = target.value;
}
contents.value = value;
}
if (event.target instanceof HTMLTextAreaElement) {
contents.value = event.target.value;
}
if (event.target instanceof HTMLSelectElement) {
contents.value = retriveSelectValue(event.target).join(",");
}
// Ensure the serializer isn't quirky
if (contents.value === undefined) {
contents.value = "";
}
return contents;
}
function serializeWheelEvent(event: WheelEvent): SerializedEvent {
return {
delta_x: event.deltaX,
delta_y: event.deltaY,
delta_z: event.deltaZ,
delta_mode: event.deltaMode,
};
}
function serializeTouchEvent(event: TouchEvent): SerializedEvent {
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,
};
}
function serializePointerEvent(event: PointerEvent): SerializedEvent {
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,
};
}
function serializeMouseEvent(event: MouseEvent): SerializedEvent {
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,
};
}
function serializeKeyboardEvent(event: KeyboardEvent): SerializedEvent {
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,
};
}
function serializeAnimationEvent(event: AnimationEvent): SerializedEvent {
return {
animation_name: event.animationName,
elapsed_time: event.elapsedTime,
pseudo_element: event.pseudoElement,
};
}
return {};
function serializeDragEvent(event: DragEvent): SerializedEvent {
// 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 };
return {
mouse: {
alt_key: event.altKey,
ctrl_key: event.ctrlKey,
meta_key: event.metaKey,
shift_key: event.shiftKey,
...serializeMouseEvent(event),
},
files: {
files: {
"a": [1, 2, 3],
}
},
};
}
+14 -15
View File
@@ -17,37 +17,36 @@ export function setAttributeInner(node: HTMLElement, field: string, value: strin
// 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);
// @ts-ignore
if (node.value !== value) {
// @ts-ignore
node.value = value;
}
break;
case "initial_value":
node.setAttribute("defaultValue", value);
// @ts-ignore
node.defaultValue = value;
break;
case "checked":
if (node instanceof HTMLInputElement) {
node.checked = truthy(value);
}
// @ts-ignore
node.checked = truthy(value);
break;
case "initial_checked":
if (node instanceof HTMLInputElement) {
node.defaultChecked = truthy(value);
}
// @ts-ignore
node.defaultChecked = truthy(value);
break;
case "selected":
if (node instanceof HTMLOptionElement) {
node.selected = truthy(value);
}
// @ts-ignore
node.selected = truthy(value);
break;
case "initial_selected":
if (node instanceof HTMLOptionElement) {
node.defaultSelected = truthy(value);
}
// @ts-ignore
node.defaultSelected = truthy(value);
break;
case "dangerous_inner_html":