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

+2 -5
View File
@@ -43,10 +43,8 @@ fn app() -> Element {
checked: enable_directory_upload, checked: enable_directory_upload,
oninput: move |evt| enable_directory_upload.set(evt.checked()), oninput: move |evt| enable_directory_upload.set(evt.checked()),
}, },
label {
r#for: "directory-upload", label { r#for: "directory-upload", "Enable directory upload" }
"Enable directory upload"
}
input { input {
r#type: "file", r#type: "file",
@@ -60,7 +58,6 @@ fn app() -> Element {
// cheating with a little bit of JS... // cheating with a little bit of JS...
"ondragover": "this.style.backgroundColor='#88FF88';", "ondragover": "this.style.backgroundColor='#88FF88';",
"ondragleave": "this.style.backgroundColor='#FFFFFF';", "ondragleave": "this.style.backgroundColor='#FFFFFF';",
id: "drop-zone", id: "drop-zone",
prevent_default: "ondrop dragover dragenter", prevent_default: "ondrop dragover dragenter",
ondrop: handle_file_drop, ondrop: handle_file_drop,
+73 -4
View File
@@ -15,23 +15,92 @@ fn app() -> Element {
rsx! { rsx! {
div { div {
h1 { "Form" } h1 { "Form" }
// The form element is used to create an HTML form for user input
// You can attach regular attributes to it
form { form {
oninput: move |ev| values.set(ev.values()), id: "cool-form",
style: "display: flex; flex-direction: column;",
// You can attach a handler to the entire form
oninput: move |ev| {
println!("Input event: {:#?}", ev);
values.set(ev.values());
},
// On desktop/liveview, the form will not navigate the page - the expectation is that you handle
// The form event.
// Howver, if your form doesn't have a submit handler, it might navigate the page depending on the webview.
// We suggest always attaching a submit handler to the form.
onsubmit: move |ev| {
println!("Submit event: {:#?}", ev);
},
// Regular text inputs with handlers
label { r#for: "username", "Username" }
input { input {
r#type: "text", r#type: "text",
name: "username", name: "username",
oninput: move |ev| values.set(ev.values()) oninput: move |ev| {
println!("setting username");
values.set(ev.values());
}
} }
// And then the various inputs that might exist
// Note for a value to be returned in .values(), it must be named!
label { r#for: "full-name", "Full Name" }
input { r#type: "text", name: "full-name" } input { r#type: "text", name: "full-name" }
label { r#for: "email", "Email" }
input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" }
label { r#for: "password", "Password" }
input { r#type: "password", name: "password" } input { r#type: "password", name: "password" }
input { r#type: "radio", name: "color", value: "red" }
label { r#for: "color", "Color" }
input { r#type: "radio", checked: true, name: "color", value: "red" }
input { r#type: "radio", name: "color", value: "blue" } input { r#type: "radio", name: "color", value: "blue" }
input { r#type: "radio", name: "color", value: "green" }
// Select multiple comes in as a comma separated list of selected values
// You should split them on the comma to get the values manually
label { r#for: "country", "Country" }
select {
name: "country",
multiple: true,
oninput: move |ev| {
println!("Input event: {:#?}", ev);
println!("Values: {:#?}", ev.value().split(',').collect::<Vec<_>>());
},
option { value: "usa", "USA" }
option { value: "canada", "Canada" }
option { value: "mexico", "Mexico" }
}
// Safari can be quirky with color inputs on mac.
// We recommend always providing a text input for color as a fallback.
label { r#for: "color", "Color" }
input { r#type: "color", value: "#000002", name: "head", id: "head" }
// Dates!
input {
min: "2018-01-01",
value: "2018-07-22",
r#type: "date",
name: "trip-start",
max: "2025-12-31",
id: "start"
}
// Buttons will submit your form by default.
button { r#type: "submit", value: "Submit", "Submit the form" } button { r#type: "submit", value: "Submit", "Submit the form" }
} }
} }
div { div {
h1 { "Oninput Values" } h1 { "Oninput Values" }
"{values:#?}" pre { "{values:#?}" }
} }
} }
} }
+96 -13
View File
@@ -20,6 +20,10 @@ fn app() -> Element {
let received = RECEIVED_EVENTS(); let received = RECEIVED_EVENTS();
let expected = utils::EXPECTED_EVENTS(); let expected = utils::EXPECTED_EVENTS();
use_effect(move || {
println!("expecting {} events", utils::EXPECTED_EVENTS());
});
if expected != 0 && received == expected { if expected != 0 && received == expected {
println!("all events recieved"); println!("all events recieved");
desktop_context.close(); desktop_context.close();
@@ -41,11 +45,15 @@ fn app() -> Element {
test_focus_in_div {} test_focus_in_div {}
test_focus_out_div {} test_focus_out_div {}
test_form_input {} test_form_input {}
test_form_submit {}
test_select_multiple_options {}
} }
} }
} }
fn test_mounted() -> Element { fn test_mounted() -> Element {
use_hook(|| utils::EXPECTED_EVENTS.with_mut(|x| *x += 1));
rsx! { rsx! {
div { div {
width: "100px", width: "100px",
@@ -433,20 +441,24 @@ fn test_form_input() -> Element {
utils::mock_event_with_extra( utils::mock_event_with_extra(
"form-username", "form-username",
r#"new Event("input", { r#"new Event("input", { bubbles: true, cancelable: true, composed: true })"#,
bubbles: true,
cancelable: true,
composed: true,
})"#,
r#"element.value = "hello";"#, r#"element.value = "hello";"#,
); );
let set_values = move |ev: FormEvent| { let set_username = move |ev: FormEvent| {
values.set(ev.values()); values.set(ev.values());
eprintln!("values: {:?}", values);
// The value of the input should match
assert_eq!(ev.value(), "hello");
// And then the value the form gives us should also match
values.with_mut(|x| { values.with_mut(|x| {
assert_eq!(x.get("username").unwrap().deref(), &["hello"]); assert_eq!(x.get("username").unwrap().deref(), "hello");
assert_eq!(x.get("full-name").unwrap().deref(), "lorem");
assert_eq!(x.get("password").unwrap().deref(), "ipsum");
assert_eq!(x.get("color").unwrap().deref(), "red");
}); });
RECEIVED_EVENTS.with_mut(|x| *x += 1);
}; };
rsx! { rsx! {
@@ -454,19 +466,90 @@ fn test_form_input() -> Element {
h1 { "Form" } h1 { "Form" }
form { form {
id: "form", id: "form",
oninput: move |ev| values.set(ev.values()), oninput: move |ev| {
values.set(ev.values());
},
onsubmit: move |ev| {
println!("{:?}", ev);
},
input { input {
r#type: "text", r#type: "text",
name: "username", name: "username",
id: "form-username", id: "form-username",
oninput: set_values, oninput: set_username,
} }
input { r#type: "text", name: "full-name" } input { r#type: "text", name: "full-name", value: "lorem" }
input { r#type: "password", name: "password" } input { r#type: "password", name: "password", value: "ipsum" }
input { r#type: "radio", name: "color", value: "red" } input { r#type: "radio", name: "color", value: "red", checked: true }
input { r#type: "radio", name: "color", value: "blue" } input { r#type: "radio", name: "color", value: "blue" }
button { r#type: "submit", value: "Submit", "Submit the form" } button { r#type: "submit", value: "Submit", "Submit the form" }
} }
} }
} }
} }
fn test_form_submit() -> Element {
let mut values = use_signal(|| HashMap::new());
utils::mock_event_with_extra(
"form-submitter",
r#"new Event("submit", { bubbles: true, cancelable: true, composed: true })"#,
r#"element.submit();"#,
);
let set_values = move |ev: FormEvent| {
values.set(ev.values());
values.with_mut(|x| {
assert_eq!(x.get("username").unwrap().deref(), "goodbye");
assert_eq!(x.get("full-name").unwrap().deref(), "lorem");
assert_eq!(x.get("password").unwrap().deref(), "ipsum");
assert_eq!(x.get("color").unwrap().deref(), "red");
});
RECEIVED_EVENTS.with_mut(|x| *x += 1);
};
rsx! {
div {
h1 { "Form" }
form {
id: "form-submitter",
onsubmit: set_values,
input { r#type: "text", name: "username", id: "username", value: "goodbye" }
input { r#type: "text", name: "full-name", value: "lorem" }
input { r#type: "password", name: "password", value: "ipsum" }
input { r#type: "radio", name: "color", value: "red", checked: true }
input { r#type: "radio", name: "color", value: "blue" }
button { r#type: "submit", value: "Submit", "Submit the form" }
}
}
}
}
fn test_select_multiple_options() -> Element {
utils::mock_event_with_extra(
"select-many",
r#"new Event("input", { bubbles: true, cancelable: true, composed: true })"#,
r#"
document.getElementById('usa').selected = true;
document.getElementById('canada').selected = true;
document.getElementById('mexico').selected = false;
"#,
);
rsx! {
select {
id: "select-many",
name: "country",
multiple: true,
oninput: move |ev| {
let values = ev.value();
let values = values.split(',').collect::<Vec<_>>();
assert_eq!(values, vec!["usa", "canada"]);
RECEIVED_EVENTS.with_mut(|x| *x += 1);
},
option { id: "usa", value: "usa", "USA" }
option { id: "canada", value: "canada", "Canada" }
option { id: "mexico", value: "mexico", selected: true, "Mexico" }
}
}
}
+6 -10
View File
@@ -30,23 +30,19 @@ pub fn mock_event(id: &'static str, value: &'static str) {
} }
pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'static str) { pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'static str) {
EXPECTED_EVENTS.with_mut(|x| *x += 1);
use_hook(move || { use_hook(move || {
EXPECTED_EVENTS.with_mut(|x| *x += 1);
spawn(async move { spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(5000)).await; tokio::time::sleep(std::time::Duration::from_millis(5000)).await;
let js = format!( let js = format!(
r#" r#"
//console.log("ran"); let event = {value};
// Dispatch a synthetic event let element = document.getElementById('{id}');
let event = {}; {extra}
let element = document.getElementById('{}');
console.log(element, event);
{}
element.dispatchEvent(event); element.dispatchEvent(event);
"#, "#
value, id, extra
); );
eval(&js).await.unwrap(); eval(&js).await.unwrap();
+2 -2
View File
@@ -1,5 +1,5 @@
use crate::{assets::*, edits::EditQueue}; use crate::{assets::*, edits::EditQueue};
use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; use dioxus_interpreter_js::unified_bindings::{native_js, SLEDGEHAMMER_JS};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use wry::{ use wry::{
http::{status::StatusCode, Request, Response}, http::{status::StatusCode, Request, Response},
@@ -38,7 +38,7 @@ fn handle_edits_code() -> String {
}}"# }}"#
); );
let mut interpreter = SLEDGEHAMMER_JS let mut interpreter = native_js()
.replace("/*POST_HANDLE_EDITS*/", PREVENT_FILE_UPLOAD) .replace("/*POST_HANDLE_EDITS*/", PREVENT_FILE_UPLOAD)
.replace("export", "") .replace("export", "")
+ &polling_request; + &polling_request;
+2
View File
@@ -110,6 +110,8 @@ impl PointerInteraction for DragData {
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedDragData { pub struct SerializedDragData {
mouse: crate::point_interaction::SerializedPointInteraction, mouse: crate::point_interaction::SerializedPointInteraction,
#[serde(default)]
files: Option<crate::file_data::SerializedFileEngine>, files: Option<crate::file_data::SerializedFileEngine>,
} }
+33 -51
View File
@@ -1,56 +1,11 @@
use crate::file_data::FileEngine; use crate::file_data::FileEngine;
use crate::file_data::HasFileData; use crate::file_data::HasFileData;
use std::ops::Deref;
use std::{collections::HashMap, fmt::Debug}; use std::{collections::HashMap, fmt::Debug};
use dioxus_core::Event; use dioxus_core::Event;
pub type FormEvent = Event<FormData>; pub type FormEvent = Event<FormData>;
/// A form value that may either be a list of values or a single value
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
// this will serialize Text(String) -> String and VecText(Vec<String>) to Vec<String>
serde(untagged)
)]
#[derive(Debug, Clone, PartialEq)]
pub enum FormValue {
Text(String),
VecText(Vec<String>),
}
impl From<FormValue> for Vec<String> {
fn from(value: FormValue) -> Self {
match value {
FormValue::Text(s) => vec![s],
FormValue::VecText(vec) => vec,
}
}
}
impl Deref for FormValue {
type Target = [String];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl FormValue {
/// Convenient way to represent Value as slice
pub fn as_slice(&self) -> &[String] {
match self {
FormValue::Text(s) => std::slice::from_ref(s),
FormValue::VecText(vec) => vec.as_slice(),
}
}
/// Convert into Vec<String>
pub fn to_vec(self) -> Vec<String> {
self.into()
}
}
/* DOMEvent: Send + SyncTarget relatedTarget */ /* DOMEvent: Send + SyncTarget relatedTarget */
pub struct FormData { pub struct FormData {
inner: Box<dyn HasFormData>, inner: Box<dyn HasFormData>,
@@ -73,6 +28,7 @@ impl Debug for FormData {
f.debug_struct("FormEvent") f.debug_struct("FormEvent")
.field("value", &self.value()) .field("value", &self.value())
.field("values", &self.values()) .field("values", &self.values())
.field("valid", &self.valid())
.finish() .finish()
} }
} }
@@ -106,8 +62,10 @@ impl FormData {
self.value().parse().unwrap_or(false) self.value().parse().unwrap_or(false)
} }
/// Get the values of the form event /// Collect all the named form values from the containing form.
pub fn values(&self) -> HashMap<String, FormValue> { ///
/// Every input must be named!
pub fn values(&self) -> HashMap<String, String> {
self.inner.values() self.inner.values()
} }
@@ -120,6 +78,11 @@ impl FormData {
pub fn downcast<T: 'static>(&self) -> Option<&T> { pub fn downcast<T: 'static>(&self) -> Option<&T> {
self.inner.as_any().downcast_ref::<T>() self.inner.as_any().downcast_ref::<T>()
} }
/// Did this form pass its own validation?
pub fn valid(&self) -> bool {
self.inner.value().is_empty()
}
} }
/// An object that has all the data for a form event /// An object that has all the data for a form event
@@ -128,7 +91,11 @@ pub trait HasFormData: HasFileData + std::any::Any {
Default::default() Default::default()
} }
fn values(&self) -> HashMap<String, FormValue> { fn valid(&self) -> bool {
true
}
fn values(&self) -> HashMap<String, String> {
Default::default() Default::default()
} }
@@ -164,8 +131,16 @@ impl FormData {
/// A serialized form data object /// A serialized form data object
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedFormData { pub struct SerializedFormData {
#[serde(default)]
value: String, value: String,
values: HashMap<String, FormValue>,
#[serde(default)]
values: HashMap<String, String>,
#[serde(default)]
valid: bool,
#[serde(default)]
files: Option<crate::file_data::SerializedFileEngine>, files: Option<crate::file_data::SerializedFileEngine>,
} }
@@ -174,13 +149,14 @@ impl SerializedFormData {
/// Create a new serialized form data object /// Create a new serialized form data object
pub fn new( pub fn new(
value: String, value: String,
values: HashMap<String, FormValue>, values: HashMap<String, String>,
files: Option<crate::file_data::SerializedFileEngine>, files: Option<crate::file_data::SerializedFileEngine>,
) -> Self { ) -> Self {
Self { Self {
value, value,
values, values,
files, files,
valid: true,
} }
} }
@@ -189,6 +165,7 @@ impl SerializedFormData {
Self { Self {
value: data.value(), value: data.value(),
values: data.values(), values: data.values(),
valid: data.valid(),
files: match data.files() { files: match data.files() {
Some(files) => { Some(files) => {
let mut resolved_files = HashMap::new(); let mut resolved_files = HashMap::new();
@@ -211,6 +188,7 @@ impl SerializedFormData {
Self { Self {
value: data.value(), value: data.value(),
values: data.values(), values: data.values(),
valid: data.valid(),
files: None, files: None,
} }
} }
@@ -222,10 +200,14 @@ impl HasFormData for SerializedFormData {
self.value.clone() self.value.clone()
} }
fn values(&self) -> HashMap<String, FormValue> { fn values(&self) -> HashMap<String, String> {
self.values.clone() self.values.clone()
} }
fn valid(&self) -> bool {
self.valid
}
fn as_any(&self) -> &dyn std::any::Any { fn as_any(&self) -> &dyn std::any::Any {
self self
} }
+13 -2
View File
@@ -34,8 +34,19 @@ impl<'de> Deserialize<'de> for HtmlEvent {
data, data,
} = Inner::deserialize(deserializer)?; } = Inner::deserialize(deserializer)?;
// in debug mode let's try and be helpful as to why the deserialization failed
#[cfg(debug_assertions)]
{
_ = deserialize_raw(&name, data.clone()).unwrap_or_else(|e| {
panic!(
"Failed to deserialize event data for event {}: {:#?}\n'{:#?}'",
name, e, data,
)
});
}
Ok(HtmlEvent { Ok(HtmlEvent {
data: fun_name(&name, data).unwrap(), data: deserialize_raw(&name, data).unwrap(),
element, element,
bubbles, bubbles,
name, name,
@@ -44,7 +55,7 @@ impl<'de> Deserialize<'de> for HtmlEvent {
} }
#[cfg(feature = "serialize")] #[cfg(feature = "serialize")]
fn fun_name( fn deserialize_raw(
name: &str, name: &str,
data: serde_value::Value, data: serde_value::Value,
) -> Result<EventData, serde_value::DeserializerError> { ) -> Result<EventData, serde_value::DeserializerError> {
+4 -2
View File
@@ -27,13 +27,15 @@ This crate features bindings for the web and sledgehammer for increased performa
## Architecture ## Architecture
We use TypeScript to write the bindings and a very simple build.rs to convert them to javascript, minify them, and glue them into the rest of the project. We use TypeScript to write the bindings and a very simple build.rs along with bun to convert them to javascript, minify them, and glue them into the rest of the project.
Not every snippet of JS will be used, so we split out the snippets from the core interpreter. Not every snippet of JS will be used, so we split out the snippets from the core interpreter.
In theory, we *could* use Rust in the browser to do everything these bindings are doing. In reality, we want to stick with JS to skip the need for a WASM build step when running the LiveView and WebView renderers. We also want to use JS to prevent diverging behavior of things like canceling events, uploading files, and collecting form inputs. These details are tough to ensure 1:1 compatibility when implementing them in two languages. In theory, we *could* use Rust in the browser to do everything these bindings are doing. In reality, we want to stick with JS to skip the need for a WASM build step when running the LiveView and WebView renderers. We also want to use JS to prevent diverging behavior of things like canceling events, uploading files, and collecting form inputs. These details are tough to ensure 1:1 compatibility when implementing them in two languages.
If you want to contribute to the bindings, you'll need to have the typescript compiler installed on your machine, accessible via `tsc`. If you want to contribute to the bindings, you'll need to have the typescript compiler installed on your machine as well as bun:
https://bun.sh/docs/installation
## Contributing ## Contributing
+11 -9
View File
@@ -6,8 +6,7 @@ use std::{
fn main() { fn main() {
// If any TS changes, re-run the build script // If any TS changes, re-run the build script
println!("cargo:rerun-if-changed=src/ts/*.ts"); println!("cargo:rerun-if-changed=src/ts/*.ts,*.json");
println!("cargo:rerun-if-changed=*.json");
// Compute the hash of the ts files // Compute the hash of the ts files
let hash = hash_dir("src/ts"); let hash = hash_dir("src/ts");
@@ -21,8 +20,8 @@ fn main() {
// Otherwise, generate the bindings and write the new hash to disk // Otherwise, generate the bindings and write the new hash to disk
// Generate the bindings for both native and web // Generate the bindings for both native and web
gen_bindings("native"); gen_bindings("interpreter_native", "native");
gen_bindings("web"); gen_bindings("interpreter_web", "web");
std::fs::write("src/js/hash.txt", hash.to_string()).unwrap(); std::fs::write("src/js/hash.txt", hash.to_string()).unwrap();
} }
@@ -51,18 +50,21 @@ fn hash_dir(dir: &str) -> u64 {
// we need to hash each of the .ts files and add that hash to the JS files // we need to hash each of the .ts files and add that hash to the JS files
// if the hashes don't match, we need to fail the build // if the hashes don't match, we need to fail the build
// that way we also don't need // that way we also don't need
fn gen_bindings(name: &str) { fn gen_bindings(input_name: &str, output_name: &str) {
// If the file is generated, and the hash is different, we need to generate it // If the file is generated, and the hash is different, we need to generate it
let status = Command::new("tsc") let status = Command::new("bun")
.arg("--p") .arg("build")
.arg(format!("tsconfig.{name}.json")) .arg(format!("src/ts/{input_name}.ts"))
.arg("--outfile")
.arg(format!("src/js/{output_name}.js"))
// .arg("--minify")
.status() .status()
.unwrap(); .unwrap();
if !status.success() { if !status.success() {
panic!( panic!(
"Failed to generate bindings for {}. Make sure you have tsc installed", "Failed to generate bindings for {}. Make sure you have tsc installed",
name input_name
); );
} }
} }
+1 -1
View File
@@ -1 +1 @@
13493771420133770074 4429706825984325407
File diff suppressed because it is too large. Load diff
+129 -158
View File
@@ -1,159 +1,130 @@
var __extends = (this && this.__extends) || (function () { // src/ts/interpreter_core.ts
var extendStatics = function (d, b) { class Interpreter {
extendStatics = Object.setPrototypeOf || global;
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || local;
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; root;
return extendStatics(d, b); handler;
}; nodes;
return function (d, b) { stack;
if (typeof b !== "function" && b !== null) templates;
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); constructor(root, handler) {
extendStatics(d, b); this.handler = handler;
function __() { this.constructor = d; } this.initialize(root);
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }
}; initialize(root) {
})(); this.global = {};
System.register("interpreter_core", [], function (exports_1, context_1) { this.local = {};
"use strict"; this.root = root;
var Interpreter; this.nodes = [root];
var __moduleName = context_1 && context_1.id; this.stack = [root];
return { this.templates = {};
setters: [], }
execute: function () { createListener(event_name, element, bubbles) {
Interpreter = (function () { if (bubbles) {
function Interpreter(root, handler) { if (this.global[event_name] === undefined) {
this.root = root; this.global[event_name] = { active: 1, callback: this.handler };
this.nodes = [root]; this.root.addEventListener(event_name, this.handler);
this.stack = [root]; } else {
this.global = {}; this.global[event_name].active++;
this.local = {}; }
this.handler = handler; } else {
} const id = element.getAttribute("data-dioxus-id");
Interpreter.prototype.createListener = function (event_name, element, bubbles) { if (!this.local[id]) {
if (bubbles) { this.local[id] = {};
if (this.global[event_name] === undefined) { }
this.global[event_name] = { active: 1, callback: this.handler }; element.addEventListener(event_name, this.handler);
this.root.addEventListener(event_name, this.handler); }
} }
else { removeListener(element, event_name, bubbles) {
this.global[event_name].active++; if (bubbles) {
} this.removeBubblingListener(event_name);
} } else {
else { this.removeNonBubblingListener(element, event_name);
var id = element.getAttribute("data-dioxus-id"); }
if (!this.local[id]) { }
this.local[id] = {}; removeBubblingListener(event_name) {
} this.global[event_name].active--;
element.addEventListener(event_name, this.handler); if (this.global[event_name].active === 0) {
} this.root.removeEventListener(event_name, this.global[event_name].callback);
}; delete this.global[event_name];
Interpreter.prototype.removeListener = function (element, event_name, bubbles) { }
if (bubbles) { }
this.removeBubblingListener(event_name); removeNonBubblingListener(element, event_name) {
} const id = element.getAttribute("data-dioxus-id");
else { delete this.local[id][event_name];
this.removeNonBubblingListener(element, event_name); if (Object.keys(this.local[id]).length === 0) {
} delete this.local[id];
}; }
Interpreter.prototype.removeBubblingListener = function (event_name) { element.removeEventListener(event_name, this.handler);
this.global[event_name].active--; }
if (this.global[event_name].active === 0) { removeAllNonBubblingListeners(element) {
this.root.removeEventListener(event_name, this.global[event_name].callback); const id = element.getAttribute("data-dioxus-id");
delete this.global[event_name]; delete this.local[id];
} }
}; getNode(id) {
Interpreter.prototype.removeNonBubblingListener = function (element, event_name) { return this.nodes[id];
var id = element.getAttribute("data-dioxus-id"); }
delete this.local[id][event_name]; appendChildren(id, many) {
if (Object.keys(this.local[id]).length === 0) { const root = this.nodes[id];
delete this.local[id]; const els = this.stack.splice(this.stack.length - many);
} for (let k = 0;k < many; k++) {
element.removeEventListener(event_name, this.handler); root.appendChild(els[k]);
}; }
Interpreter.prototype.removeAllNonBubblingListeners = function (element) { }
var id = element.getAttribute("data-dioxus-id"); }
delete this.local[id];
}; // src/ts/interpreter_web.ts
Interpreter.prototype.getNode = function (id) { class PlatformInterpreter extends Interpreter {
return this.nodes[id]; m;
}; constructor(root, handler) {
Interpreter.prototype.appendChildren = function (id, many) { super(root, handler);
var root = this.nodes[id]; }
var els = this.stack.splice(this.stack.length - many); LoadChild(ptr, len) {
for (var k = 0; k < many; k++) { let node = this.stack[this.stack.length - 1];
root.appendChild(els[k]); let ptr_end = ptr + len;
} for (;ptr < ptr_end; ptr++) {
}; let end = this.m.getUint8(ptr);
return Interpreter; for (node = node.firstChild;end > 0; end--) {
}()); node = node.nextSibling;
exports_1("Interpreter", Interpreter); }
}
return node;
}
saveTemplate(nodes, tmpl_id) {
this.templates[tmpl_id] = nodes;
}
hydrateRoot(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])];
this.nodes[id] = hydrateNode;
if (split.length > 1) {
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);
} }
}; }
}); }
System.register("interpreter_web", ["interpreter_core"], function (exports_2, context_2) { const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
"use strict"; let currentNode = treeWalker.nextNode();
var interpreter_core_1, WebInterpreter; while (currentNode) {
var __moduleName = context_2 && context_2.id; const id = currentNode.textContent;
return { const split = id.split("node-id");
setters: [ if (split.length > 1) {
function (interpreter_core_1_1) { this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling;
interpreter_core_1 = interpreter_core_1_1; }
} currentNode = treeWalker.nextNode();
], }
execute: function () { }
WebInterpreter = (function (_super) { }
__extends(WebInterpreter, _super); export {
function WebInterpreter(root, handler) { PlatformInterpreter
return _super.call(this, root, handler) || this; };
}
WebInterpreter.prototype.LoadChild = function (ptr, len) {
var node = this.stack[this.stack.length - 1];
var ptr_end = ptr + len;
for (; ptr < ptr_end; ptr++) {
var end = this.m.getUint8(ptr);
for (node = node.firstChild; end > 0; end--) {
node = node.nextSibling;
}
}
return node;
};
WebInterpreter.prototype.saveTemplate = function (nodes, tmpl_id) {
this.templates[tmpl_id] = nodes;
};
WebInterpreter.prototype.hydrateRoot = function (ids) {
var hydrateNodes = document.querySelectorAll('[data-node-hydration]');
for (var i = 0; i < hydrateNodes.length; i++) {
var hydrateNode = hydrateNodes[i];
var hydration = hydrateNode.getAttribute('data-node-hydration');
var split = hydration.split(',');
var id = ids[parseInt(split[0])];
this.nodes[id] = hydrateNode;
if (split.length > 1) {
hydrateNode.listening = split.length - 1;
hydrateNode.setAttribute('data-dioxus-id', id.toString());
for (var j = 1; j < split.length; j++) {
var listener = split[j];
var split2 = listener.split(':');
var event_name = split2[0];
var bubbles = split2[1] === '1';
this.createListener(event_name, hydrateNode, bubbles);
}
}
}
var treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
var currentNode = treeWalker.nextNode();
while (currentNode) {
var id = currentNode.textContent;
var split = id.split('node-id');
if (split.length > 1) {
this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling;
}
currentNode = treeWalker.nextNode();
}
};
return WebInterpreter;
}(interpreter_core_1.Interpreter));
exports_2("WebInterpreter", WebInterpreter);
}
};
});
+1
View File
@@ -24,6 +24,7 @@ pub mod minimal_bindings {
/// Set the attribute of the node /// Set the attribute of the node
pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>); pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>);
/// Roll up all the values from the node into a JS object that we can deserialize
pub fn collectFormValues(node: JsValue) -> JsValue; pub fn collectFormValues(node: JsValue) -> JsValue;
} }
} }
+41 -36
View File
@@ -1,53 +1,58 @@
// Consistently deserialize forms and form elements for use across web/desktop/mobile // 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 { 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")) { // If there's a form...
retrieveFormValues(target, contents); let form = target.closest("form");
}
if (target instanceof HTMLSelectElement && (event.type === "input" || event.type === "change")) { // If the target is an input, and the event is input or change, we want to get the value without going through the form
retriveInputsValues(target, contents); if (form) {
if (
event.type === "input"
|| event.type === "change"
|| event.type === "submit"
|| event.type === "reset"
|| event.type === "click"
) {
contents = retrieveFormValues(form);
}
} }
return contents; 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); const formData = new FormData(form);
const contents: { [key: string]: FormDataEntryValue } = {};
for (let name in formData.keys()) { formData.forEach((value, key) => {
let element = form.elements.namedItem(name); if (contents[key]) {
contents[key] += "," + value;
// todo: this is going to be a problem for select-multiple? } else {
if (!(element instanceof HTMLInputElement)) { contents[key] = value;
continue;
} }
});
switch (element.type) { return {
case "select-multiple": valid: form.checkValidity(),
contents[name] = formData.getAll(name); values: contents
break; };
// By default, it's just a single value
default:
contents[name] = [formData.get(name)];
break;
}
}
} }
export function retriveInputsValues(target: HTMLSelectElement, contents: FormValues,) { export function retriveSelectValue(target: HTMLSelectElement): string[] {
const selectData = target.options; // there might be multiple...
contents["options"] = []; let options = target.selectedOptions;
let values = [];
for (let i = 0; i < selectData.length; i++) { for (let i = 0; i < options.length; i++) {
let option = selectData[i]; values.push(options[i].value);
if (option.selected) {
contents["options"].push(option.value.toString());
}
} }
return values;
} }
@@ -1,6 +1,8 @@
// The root interpreter class that holds state about the mapping between DOM and VirtualDom // 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 // 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 { export class Interpreter {
// non bubbling events listen at the element the listener was created at // non bubbling events listen at the element the listener was created at
global: { global: {
@@ -22,12 +24,18 @@ export class Interpreter {
}; };
constructor(root: HTMLElement, handler: EventListener) { constructor(root: HTMLElement, handler: EventListener) {
this.root = root; this.handler = handler;
this.nodes = [root]; this.initialize(root);
this.stack = [root]; }
initialize(root: HTMLElement) {
this.global = {}; this.global = {};
this.local = {}; this.local = {};
this.handler = handler; this.root = root;
this.nodes = [root];
this.stack = [root];
this.templates = {};
} }
createListener(event_name: string, element: HTMLElement, bubbles: boolean) { createListener(event_name: string, element: HTMLElement, bubbles: boolean) {
@@ -77,11 +85,11 @@ export class Interpreter {
delete this.local[id]; delete this.local[id];
} }
getNode(id: number): Node { getNode(id: NodeId): Node {
return this.nodes[id]; return this.nodes[id];
} }
appendChildren(id: number, many: number) { appendChildren(id: NodeId, many: number) {
const root = this.nodes[id]; const root = this.nodes[id];
const els = this.stack.splice(this.stack.length - many); const els = this.stack.splice(this.stack.length - many);
for (let k = 0; k < many; k++) { 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 // 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. // provide since it doesn't have access to the dom.
import { retriveValues } from "./form"; import { Interpreter, NodeId } from "./interpreter_core";
import { Interpreter } from "./interpreter_core";
import { SerializedEvent, serializeEvent } from "./serialize"; import { SerializedEvent, serializeEvent } from "./serialize";
import { setAttributeInner } from "./set_attribute";
export class PlatformInterpreter extends Interpreter {
export class NativeInterpreter extends Interpreter {
intercept_link_redirects: boolean; intercept_link_redirects: boolean;
ipc: any; ipc: any;
@@ -21,6 +20,23 @@ export class NativeInterpreter extends Interpreter {
this.intercept_link_redirects = true; this.intercept_link_redirects = true;
this.liveview = false; 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 // @ts-ignore - wry gives us this
this.ipc = window.ipc; this.ipc = window.ipc;
} }
@@ -29,14 +45,18 @@ export class NativeInterpreter extends Interpreter {
return JSON.stringify({ method, params }); 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]; const node = this.nodes[id];
if (node instanceof HTMLElement) { if (node instanceof HTMLElement) {
node.scrollIntoView({ behavior }); node.scrollIntoView({ behavior });
} }
} }
getClientRect(id: number) { getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined {
const node = this.nodes[id]; const node = this.nodes[id];
if (node instanceof HTMLElement) { if (node instanceof HTMLElement) {
const rect = node.getBoundingClientRect(); 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]; const node = this.nodes[id];
if (node instanceof HTMLElement) { if (node instanceof HTMLElement) {
@@ -74,7 +94,7 @@ export class NativeInterpreter extends Interpreter {
return node; return node;
} }
AppendChildren(id: number, many: number) { AppendChildren(id: NodeId, many: number) {
const root = this.nodes[id]; const root = this.nodes[id];
const els = this.stack.splice(this.stack.length - many); 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) { handleEvent(event: Event, name: string, bubbles: boolean) {
const target = event.target!; const target = event.target!;
const realId = targetId(target)!; const realId = targetId(target)!;
const contents = serializeEvent(event); const contents = serializeEvent(event, target);
// 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 // Handle the event on the virtualdom and then preventDefault if it also preventsDefault
// Some listeners // Some listeners
@@ -119,21 +134,26 @@ export class NativeInterpreter extends Interpreter {
} }
} else { } else {
// Run the event handler on the virtualdom const message = this.serializeIpcMessage("user_event", body);
// capture/prevent default of the event if the virtualdom wants to this.ipc.postMessage(message);
const res = handleVirtualdomEventSync(JSON.stringify(body));
if (res.preventDefault) { // // Run the event handler on the virtualdom
event.preventDefault(); // // capture/prevent default of the event if the virtualdom wants to
} // const res = handleVirtualdomEventSync(JSON.stringify(body));
if (res.stopPropagation) { // if (res.preventDefault) {
event.stopPropagation(); // 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 files = target.files!;
let file_contents: { [name: string]: number[] } = {}; let file_contents: { [name: string]: number[] } = {};
@@ -251,7 +271,7 @@ function handleVirtualdomEventSync(contents: string): EventSyncResult {
return JSON.parse(xhr.responseText); 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 // Ensure that the target is a node, sometimes it's nota
if (!(target instanceof Node)) { if (!(target instanceof Node)) {
return null; return null;
@@ -6,7 +6,7 @@
import { Interpreter } from "./interpreter_core"; import { Interpreter } from "./interpreter_core";
export class WebInterpreter extends Interpreter { export class PlatformInterpreter extends Interpreter {
m: any; m: any;
constructor(root: HTMLElement, handler: EventListener) { constructor(root: HTMLElement, handler: EventListener) {
+202 -144
View File
@@ -1,159 +1,217 @@
// Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {}; // Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {};
import { retriveSelectValue, retriveValues } from "./form";
export type AppTouchEvent = TouchEvent;
export type SerializedEvent = { export type SerializedEvent = {
values?: { [key: string]: FormDataEntryValue[] }; values?: { [key: string]: FormDataEntryValue };
value?: string; value?: string;
[key: string]: any; [key: string]: any;
}; };
export function serializeEvent(event: Event): SerializedEvent { export function serializeEvent(event: Event, target: EventTarget): SerializedEvent {
if (event instanceof InputEvent) { let contents = {};
if (event.target instanceof HTMLInputElement) {
let target = event.target; // merge the object into the contents
let value = target.value ?? target.textContent; let extend = (obj: any) => (contents = { ...contents, ...obj });
if (target.type === "checkbox") {
value = target.checked ? "true" : "false"; if (event instanceof WheelEvent) { extend(serializeWheelEvent(event)) };
} if (event instanceof MouseEvent) { extend(serializeMouseEvent(event)) }
return { if (event instanceof KeyboardEvent) { extend(serializeKeyboardEvent(event)) }
value: value,
values: {}, if (event instanceof InputEvent) { extend(serializeInputEvent(event, target)) }
}; if (event instanceof PointerEvent) { extend(serializePointerEvent(event)) }
} if (event instanceof AnimationEvent) { extend(serializeAnimationEvent(event)) }
return {}; 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 KeyboardEvent) { if (event instanceof FocusEvent) { extend({}) }
return { if (event instanceof ClipboardEvent) { extend({}) }
char_code: event.charCode,
is_composing: event.isComposing, // safari is quirky and doesn't have TouchEvent
key: event.key, if (typeof TouchEvent !== 'undefined' && event instanceof TouchEvent) { extend(serializeTouchEvent(event)); }
alt_key: event.altKey,
ctrl_key: event.ctrlKey, if (event.type === "submit" || event.type === "reset" || event.type === "click" || event.type === "change" || event.type === "input") {
meta_key: event.metaKey, extend(serializeInputEvent(event as InputEvent, target));
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,
};
} }
// If there's any files, we need to serialize them
if (event instanceof DragEvent) { if (event instanceof DragEvent) {
// let files = []; // let files: { [key: string]: Uint8Array } = {};
// if (event.dataTransfer && event.dataTransfer.files) { // if (event.dataTransfer && event.dataTransfer.files) {
// files = ["a", "b", "c"]; // files["a"] = new Uint8Array(0);
// // files = await serializeFileList(event.dataTransfer.files); // // files = {
// } // // entries: Array.from(event.dataTransfer.files).map((file) => {
// return { mouse: get_mouse_data(event), files }; // // return {
return { // // name: file.name,
mouse: { // // type: file.type,
alt_key: event.altKey, // // size: file.size,
ctrl_key: event.ctrlKey, // // last_modified: file.lastModified,
meta_key: event.metaKey, // // };
shift_key: event.shiftKey, // // }
}, // // };
files: [], // // files = await serializeFileList(event.dataTransfer.files);
}; // }
// extend({ files: files });
} }
if (event instanceof FocusEvent) { return contents;
return {}; }
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 // A few attributes are need to be set with either boolean values or require some sort of translation
switch (field) { switch (field) {
case "value": case "value":
if (value !== node.getAttribute("value")) { // @ts-ignore
node.setAttribute(field, value); if (node.value !== value) {
// @ts-ignore
node.value = value;
} }
break; break;
case "initial_value": case "initial_value":
node.setAttribute("defaultValue", value); // @ts-ignore
node.defaultValue = value;
break; break;
case "checked": case "checked":
if (node instanceof HTMLInputElement) { // @ts-ignore
node.checked = truthy(value); node.checked = truthy(value);
}
break; break;
case "initial_checked": case "initial_checked":
if (node instanceof HTMLInputElement) { // @ts-ignore
node.defaultChecked = truthy(value); node.defaultChecked = truthy(value);
}
break; break;
case "selected": case "selected":
if (node instanceof HTMLOptionElement) { // @ts-ignore
node.selected = truthy(value); node.selected = truthy(value);
}
break; break;
case "initial_selected": case "initial_selected":
if (node instanceof HTMLOptionElement) { // @ts-ignore
node.defaultSelected = truthy(value); node.defaultSelected = truthy(value);
}
break; break;
case "dangerous_inner_html": case "dangerous_inner_html":
+14 -10
View File
@@ -6,6 +6,10 @@ use wasm_bindgen::prelude::wasm_bindgen;
use sledgehammer_bindgen::bindgen; use sledgehammer_bindgen::bindgen;
pub fn native_js() -> String {
format!("{}\n{}", include_str!("./js/native.js"), GENERATED_JS,)
}
pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
/// Extensions to the interpreter that are specific to the web platform. /// Extensions to the interpreter that are specific to the web platform.
@@ -55,7 +59,7 @@ mod js {
"{this.stack.pop();}" "{this.stack.pop();}"
} }
fn replace_with(id: u32, n: u16) { fn replace_with(id: u32, n: u16) {
"{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...this.els);}" "{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.removeAllNonBubblingListeners(root); } root.replaceWith(...this.els);}"
} }
fn insert_after(id: u32, n: u16) { fn insert_after(id: u32, n: u16) {
"{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}" "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
@@ -64,7 +68,7 @@ mod js {
"{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}" "{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}"
} }
fn remove(id: u32) { fn remove(id: u32) {
"{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.listeners.removeAllNonBubbling(node); } node.remove(); }}" "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.removeAllNonBubblingListeners(node); } node.remove(); }}"
} }
fn create_raw_text(text: &str) { fn create_raw_text(text: &str) {
"{this.stack.push(document.createTextNode($text$));}" "{this.stack.push(document.createTextNode($text$));}"
@@ -76,10 +80,10 @@ mod js {
"{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}" "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}"
} }
fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) { fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.listeners.create($event_name$, node, $bubbles$);"# r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.createListener($event_name$, node, $bubbles$);"#
} }
fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) { fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
"{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.listeners.remove(node, $event_name$, $bubbles$);}" "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.removeListener(node, $event_name$, $bubbles$);}"
} }
fn set_text(id: u32, text: &str) { fn set_text(id: u32, text: &str) {
"{this.nodes[$id$].textContent = $text$;}" "{this.nodes[$id$].textContent = $text$;}"
@@ -173,13 +177,13 @@ mod js {
fn foreign_event_listener(event: &str<u8, evt>, id: u32, bubbles: u8) { fn foreign_event_listener(event: &str<u8, evt>, id: u32, bubbles: u8) {
r#" r#"
bubbles = bubbles == 1; bubbles = bubbles == 1;
let node = this.nodes[id]; let this_node = this.nodes[id];
if(node.listening){ if(this_node.listening){
node.listening += 1; this_node.listening += 1;
} else { } else {
node.listening = 1; this_node.listening = 1;
} }
node.setAttribute('data-dioxus-id', `\${id}`); this_node.setAttribute('data-dioxus-id', `\${id}`);
const event_name = $event$; const event_name = $event$;
// if this is a mounted listener, we send the event immediately // if this is a mounted listener, we send the event immediately
@@ -193,7 +197,7 @@ mod js {
}) })
); );
} else { } else {
this.listeners.create(event_name, node, bubbles, (event) => { this.createListener(event_name, this_node, bubbles, (event) => {
this.handler(event, event_name, bubbles); this.handler(event, event_name, bubbles);
}); });
}"# }"#
@@ -181,7 +181,8 @@ impl WriteMutations for MutationState {
} }
fn create_event_listener(&mut self, name: &'static str, id: dioxus_core::ElementId) { fn create_event_listener(&mut self, name: &'static str, id: dioxus_core::ElementId) {
// note that we use the foreign event listener here // note that we use the foreign event listener here instead of the native one
// the native method assumes we have direct access to the dom, which we don't.
self.channel self.channel
.foreign_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);
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "system", "module": "CommonJS",
"lib": [ "lib": [
"ES2015", "ES2015",
"DOM", "DOM",