mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
fix navigating when files are dropped
This commit is contained in:
parent
0ff0eb7846
commit
199173a409
23 changed files with 1244 additions and 1028 deletions
|
@ -43,10 +43,8 @@ fn app() -> Element {
|
|||
checked: enable_directory_upload,
|
||||
oninput: move |evt| enable_directory_upload.set(evt.checked()),
|
||||
},
|
||||
label {
|
||||
r#for: "directory-upload",
|
||||
"Enable directory upload"
|
||||
}
|
||||
|
||||
label { r#for: "directory-upload", "Enable directory upload" }
|
||||
|
||||
input {
|
||||
r#type: "file",
|
||||
|
@ -60,7 +58,6 @@ fn app() -> Element {
|
|||
// cheating with a little bit of JS...
|
||||
"ondragover": "this.style.backgroundColor='#88FF88';",
|
||||
"ondragleave": "this.style.backgroundColor='#FFFFFF';",
|
||||
|
||||
id: "drop-zone",
|
||||
prevent_default: "ondrop dragover dragenter",
|
||||
ondrop: handle_file_drop,
|
||||
|
|
|
@ -15,23 +15,92 @@ fn app() -> Element {
|
|||
rsx! {
|
||||
div {
|
||||
h1 { "Form" }
|
||||
|
||||
// The form element is used to create an HTML form for user input
|
||||
// You can attach regular attributes to it
|
||||
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 {
|
||||
r#type: "text",
|
||||
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" }
|
||||
|
||||
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: "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: "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" }
|
||||
}
|
||||
}
|
||||
div {
|
||||
h1 { "Oninput Values" }
|
||||
"{values:#?}"
|
||||
pre { "{values:#?}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,10 @@ fn app() -> Element {
|
|||
let received = RECEIVED_EVENTS();
|
||||
let expected = utils::EXPECTED_EVENTS();
|
||||
|
||||
use_effect(move || {
|
||||
println!("expecting {} events", utils::EXPECTED_EVENTS());
|
||||
});
|
||||
|
||||
if expected != 0 && received == expected {
|
||||
println!("all events recieved");
|
||||
desktop_context.close();
|
||||
|
@ -41,11 +45,15 @@ fn app() -> Element {
|
|||
test_focus_in_div {}
|
||||
test_focus_out_div {}
|
||||
test_form_input {}
|
||||
test_form_submit {}
|
||||
test_select_multiple_options {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_mounted() -> Element {
|
||||
use_hook(|| utils::EXPECTED_EVENTS.with_mut(|x| *x += 1));
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
width: "100px",
|
||||
|
@ -433,20 +441,24 @@ fn test_form_input() -> Element {
|
|||
|
||||
utils::mock_event_with_extra(
|
||||
"form-username",
|
||||
r#"new Event("input", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
})"#,
|
||||
r#"new Event("input", { bubbles: true, cancelable: true, composed: true })"#,
|
||||
r#"element.value = "hello";"#,
|
||||
);
|
||||
|
||||
let set_values = move |ev: FormEvent| {
|
||||
let set_username = move |ev: FormEvent| {
|
||||
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| {
|
||||
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! {
|
||||
|
@ -454,19 +466,90 @@ fn test_form_input() -> Element {
|
|||
h1 { "Form" }
|
||||
form {
|
||||
id: "form",
|
||||
oninput: move |ev| values.set(ev.values()),
|
||||
oninput: move |ev| {
|
||||
values.set(ev.values());
|
||||
},
|
||||
onsubmit: move |ev| {
|
||||
println!("{:?}", ev);
|
||||
},
|
||||
input {
|
||||
r#type: "text",
|
||||
name: "username",
|
||||
id: "form-username",
|
||||
oninput: set_values,
|
||||
oninput: set_username,
|
||||
}
|
||||
input { r#type: "text", name: "full-name" }
|
||||
input { r#type: "password", name: "password" }
|
||||
input { r#type: "radio", name: "color", value: "red" }
|
||||
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_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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
EXPECTED_EVENTS.with_mut(|x| *x += 1);
|
||||
|
||||
use_hook(move || {
|
||||
EXPECTED_EVENTS.with_mut(|x| *x += 1);
|
||||
|
||||
spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(5000)).await;
|
||||
|
||||
let js = format!(
|
||||
r#"
|
||||
//console.log("ran");
|
||||
// Dispatch a synthetic event
|
||||
let event = {};
|
||||
let element = document.getElementById('{}');
|
||||
console.log(element, event);
|
||||
{}
|
||||
let event = {value};
|
||||
let element = document.getElementById('{id}');
|
||||
{extra}
|
||||
element.dispatchEvent(event);
|
||||
"#,
|
||||
value, id, extra
|
||||
"#
|
||||
);
|
||||
|
||||
eval(&js).await.unwrap();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 wry::{
|
||||
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("export", "")
|
||||
+ &polling_request;
|
||||
|
|
|
@ -110,6 +110,8 @@ impl PointerInteraction for DragData {
|
|||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct SerializedDragData {
|
||||
mouse: crate::point_interaction::SerializedPointInteraction,
|
||||
|
||||
#[serde(default)]
|
||||
files: Option<crate::file_data::SerializedFileEngine>,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,56 +1,11 @@
|
|||
use crate::file_data::FileEngine;
|
||||
use crate::file_data::HasFileData;
|
||||
use std::ops::Deref;
|
||||
use std::{collections::HashMap, fmt::Debug};
|
||||
|
||||
use dioxus_core::Event;
|
||||
|
||||
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 */
|
||||
pub struct FormData {
|
||||
inner: Box<dyn HasFormData>,
|
||||
|
@ -73,6 +28,7 @@ impl Debug for FormData {
|
|||
f.debug_struct("FormEvent")
|
||||
.field("value", &self.value())
|
||||
.field("values", &self.values())
|
||||
.field("valid", &self.valid())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
@ -106,8 +62,10 @@ impl FormData {
|
|||
self.value().parse().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Get the values of the form event
|
||||
pub fn values(&self) -> HashMap<String, FormValue> {
|
||||
/// Collect all the named form values from the containing form.
|
||||
///
|
||||
/// Every input must be named!
|
||||
pub fn values(&self) -> HashMap<String, String> {
|
||||
self.inner.values()
|
||||
}
|
||||
|
||||
|
@ -120,6 +78,11 @@ impl FormData {
|
|||
pub fn downcast<T: 'static>(&self) -> Option<&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
|
||||
|
@ -128,7 +91,11 @@ pub trait HasFormData: HasFileData + std::any::Any {
|
|||
Default::default()
|
||||
}
|
||||
|
||||
fn values(&self) -> HashMap<String, FormValue> {
|
||||
fn valid(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn values(&self) -> HashMap<String, String> {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
|
@ -164,8 +131,16 @@ impl FormData {
|
|||
/// A serialized form data object
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct SerializedFormData {
|
||||
#[serde(default)]
|
||||
value: String,
|
||||
values: HashMap<String, FormValue>,
|
||||
|
||||
#[serde(default)]
|
||||
values: HashMap<String, String>,
|
||||
|
||||
#[serde(default)]
|
||||
valid: bool,
|
||||
|
||||
#[serde(default)]
|
||||
files: Option<crate::file_data::SerializedFileEngine>,
|
||||
}
|
||||
|
||||
|
@ -174,13 +149,14 @@ impl SerializedFormData {
|
|||
/// Create a new serialized form data object
|
||||
pub fn new(
|
||||
value: String,
|
||||
values: HashMap<String, FormValue>,
|
||||
values: HashMap<String, String>,
|
||||
files: Option<crate::file_data::SerializedFileEngine>,
|
||||
) -> Self {
|
||||
Self {
|
||||
value,
|
||||
values,
|
||||
files,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,6 +165,7 @@ impl SerializedFormData {
|
|||
Self {
|
||||
value: data.value(),
|
||||
values: data.values(),
|
||||
valid: data.valid(),
|
||||
files: match data.files() {
|
||||
Some(files) => {
|
||||
let mut resolved_files = HashMap::new();
|
||||
|
@ -211,6 +188,7 @@ impl SerializedFormData {
|
|||
Self {
|
||||
value: data.value(),
|
||||
values: data.values(),
|
||||
valid: data.valid(),
|
||||
files: None,
|
||||
}
|
||||
}
|
||||
|
@ -222,10 +200,14 @@ impl HasFormData for SerializedFormData {
|
|||
self.value.clone()
|
||||
}
|
||||
|
||||
fn values(&self) -> HashMap<String, FormValue> {
|
||||
fn values(&self) -> HashMap<String, String> {
|
||||
self.values.clone()
|
||||
}
|
||||
|
||||
fn valid(&self) -> bool {
|
||||
self.valid
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
|
|
@ -34,8 +34,19 @@ impl<'de> Deserialize<'de> for HtmlEvent {
|
|||
data,
|
||||
} = 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 {
|
||||
data: fun_name(&name, data).unwrap(),
|
||||
data: deserialize_raw(&name, data).unwrap(),
|
||||
element,
|
||||
bubbles,
|
||||
name,
|
||||
|
@ -44,7 +55,7 @@ impl<'de> Deserialize<'de> for HtmlEvent {
|
|||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
fn fun_name(
|
||||
fn deserialize_raw(
|
||||
name: &str,
|
||||
data: serde_value::Value,
|
||||
) -> Result<EventData, serde_value::DeserializerError> {
|
||||
|
|
|
@ -27,13 +27,15 @@ This crate features bindings for the web and sledgehammer for increased performa
|
|||
|
||||
## 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.
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -6,8 +6,7 @@ use std::{
|
|||
|
||||
fn main() {
|
||||
// If any TS changes, re-run the build script
|
||||
println!("cargo:rerun-if-changed=src/ts/*.ts");
|
||||
println!("cargo:rerun-if-changed=*.json");
|
||||
println!("cargo:rerun-if-changed=src/ts/*.ts,*.json");
|
||||
|
||||
// Compute the hash of the ts files
|
||||
let hash = hash_dir("src/ts");
|
||||
|
@ -21,8 +20,8 @@ fn main() {
|
|||
|
||||
// Otherwise, generate the bindings and write the new hash to disk
|
||||
// Generate the bindings for both native and web
|
||||
gen_bindings("native");
|
||||
gen_bindings("web");
|
||||
gen_bindings("interpreter_native", "native");
|
||||
gen_bindings("interpreter_web", "web");
|
||||
|
||||
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
|
||||
// if the hashes don't match, we need to fail the build
|
||||
// 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
|
||||
let status = Command::new("tsc")
|
||||
.arg("--p")
|
||||
.arg(format!("tsconfig.{name}.json"))
|
||||
let status = Command::new("bun")
|
||||
.arg("build")
|
||||
.arg(format!("src/ts/{input_name}.ts"))
|
||||
.arg("--outfile")
|
||||
.arg(format!("src/js/{output_name}.js"))
|
||||
// .arg("--minify")
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
if !status.success() {
|
||||
panic!(
|
||||
"Failed to generate bindings for {}. Make sure you have tsc installed",
|
||||
name
|
||||
input_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
13493771420133770074
|
||||
4429706825984325407
|
File diff suppressed because it is too large
Load diff
|
@ -1,159 +1,130 @@
|
|||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
if (typeof b !== "function" && b !== null)
|
||||
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
System.register("interpreter_core", [], function (exports_1, context_1) {
|
||||
"use strict";
|
||||
var Interpreter;
|
||||
var __moduleName = context_1 && context_1.id;
|
||||
return {
|
||||
setters: [],
|
||||
execute: function () {
|
||||
Interpreter = (function () {
|
||||
function Interpreter(root, handler) {
|
||||
this.root = root;
|
||||
this.nodes = [root];
|
||||
this.stack = [root];
|
||||
this.global = {};
|
||||
this.local = {};
|
||||
this.handler = handler;
|
||||
}
|
||||
Interpreter.prototype.createListener = function (event_name, element, bubbles) {
|
||||
if (bubbles) {
|
||||
if (this.global[event_name] === undefined) {
|
||||
this.global[event_name] = { active: 1, callback: this.handler };
|
||||
this.root.addEventListener(event_name, this.handler);
|
||||
}
|
||||
else {
|
||||
this.global[event_name].active++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
var id = element.getAttribute("data-dioxus-id");
|
||||
if (!this.local[id]) {
|
||||
this.local[id] = {};
|
||||
}
|
||||
element.addEventListener(event_name, this.handler);
|
||||
}
|
||||
};
|
||||
Interpreter.prototype.removeListener = function (element, event_name, bubbles) {
|
||||
if (bubbles) {
|
||||
this.removeBubblingListener(event_name);
|
||||
}
|
||||
else {
|
||||
this.removeNonBubblingListener(element, event_name);
|
||||
}
|
||||
};
|
||||
Interpreter.prototype.removeBubblingListener = function (event_name) {
|
||||
this.global[event_name].active--;
|
||||
if (this.global[event_name].active === 0) {
|
||||
this.root.removeEventListener(event_name, this.global[event_name].callback);
|
||||
delete this.global[event_name];
|
||||
}
|
||||
};
|
||||
Interpreter.prototype.removeNonBubblingListener = function (element, event_name) {
|
||||
var id = element.getAttribute("data-dioxus-id");
|
||||
delete this.local[id][event_name];
|
||||
if (Object.keys(this.local[id]).length === 0) {
|
||||
delete this.local[id];
|
||||
}
|
||||
element.removeEventListener(event_name, this.handler);
|
||||
};
|
||||
Interpreter.prototype.removeAllNonBubblingListeners = function (element) {
|
||||
var id = element.getAttribute("data-dioxus-id");
|
||||
delete this.local[id];
|
||||
};
|
||||
Interpreter.prototype.getNode = function (id) {
|
||||
return this.nodes[id];
|
||||
};
|
||||
Interpreter.prototype.appendChildren = function (id, many) {
|
||||
var root = this.nodes[id];
|
||||
var els = this.stack.splice(this.stack.length - many);
|
||||
for (var k = 0; k < many; k++) {
|
||||
root.appendChild(els[k]);
|
||||
}
|
||||
};
|
||||
return Interpreter;
|
||||
}());
|
||||
exports_1("Interpreter", Interpreter);
|
||||
// src/ts/interpreter_core.ts
|
||||
class Interpreter {
|
||||
global;
|
||||
local;
|
||||
root;
|
||||
handler;
|
||||
nodes;
|
||||
stack;
|
||||
templates;
|
||||
constructor(root, handler) {
|
||||
this.handler = handler;
|
||||
this.initialize(root);
|
||||
}
|
||||
initialize(root) {
|
||||
this.global = {};
|
||||
this.local = {};
|
||||
this.root = root;
|
||||
this.nodes = [root];
|
||||
this.stack = [root];
|
||||
this.templates = {};
|
||||
}
|
||||
createListener(event_name, element, bubbles) {
|
||||
if (bubbles) {
|
||||
if (this.global[event_name] === undefined) {
|
||||
this.global[event_name] = { active: 1, callback: this.handler };
|
||||
this.root.addEventListener(event_name, this.handler);
|
||||
} else {
|
||||
this.global[event_name].active++;
|
||||
}
|
||||
} else {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
if (!this.local[id]) {
|
||||
this.local[id] = {};
|
||||
}
|
||||
element.addEventListener(event_name, this.handler);
|
||||
}
|
||||
}
|
||||
removeListener(element, event_name, bubbles) {
|
||||
if (bubbles) {
|
||||
this.removeBubblingListener(event_name);
|
||||
} else {
|
||||
this.removeNonBubblingListener(element, event_name);
|
||||
}
|
||||
}
|
||||
removeBubblingListener(event_name) {
|
||||
this.global[event_name].active--;
|
||||
if (this.global[event_name].active === 0) {
|
||||
this.root.removeEventListener(event_name, this.global[event_name].callback);
|
||||
delete this.global[event_name];
|
||||
}
|
||||
}
|
||||
removeNonBubblingListener(element, event_name) {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
delete this.local[id][event_name];
|
||||
if (Object.keys(this.local[id]).length === 0) {
|
||||
delete this.local[id];
|
||||
}
|
||||
element.removeEventListener(event_name, this.handler);
|
||||
}
|
||||
removeAllNonBubblingListeners(element) {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
delete this.local[id];
|
||||
}
|
||||
getNode(id) {
|
||||
return this.nodes[id];
|
||||
}
|
||||
appendChildren(id, many) {
|
||||
const root = this.nodes[id];
|
||||
const els = this.stack.splice(this.stack.length - many);
|
||||
for (let k = 0;k < many; k++) {
|
||||
root.appendChild(els[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// src/ts/interpreter_web.ts
|
||||
class PlatformInterpreter extends Interpreter {
|
||||
m;
|
||||
constructor(root, handler) {
|
||||
super(root, handler);
|
||||
}
|
||||
LoadChild(ptr, len) {
|
||||
let node = this.stack[this.stack.length - 1];
|
||||
let ptr_end = ptr + len;
|
||||
for (;ptr < ptr_end; ptr++) {
|
||||
let end = this.m.getUint8(ptr);
|
||||
for (node = node.firstChild;end > 0; end--) {
|
||||
node = node.nextSibling;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
"use strict";
|
||||
var interpreter_core_1, WebInterpreter;
|
||||
var __moduleName = context_2 && context_2.id;
|
||||
return {
|
||||
setters: [
|
||||
function (interpreter_core_1_1) {
|
||||
interpreter_core_1 = interpreter_core_1_1;
|
||||
}
|
||||
],
|
||||
execute: function () {
|
||||
WebInterpreter = (function (_super) {
|
||||
__extends(WebInterpreter, _super);
|
||||
function WebInterpreter(root, handler) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
currentNode = treeWalker.nextNode();
|
||||
}
|
||||
}
|
||||
}
|
||||
export {
|
||||
PlatformInterpreter
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@ pub mod minimal_bindings {
|
|||
/// Set the attribute of the node
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {};
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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],
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -6,6 +6,10 @@ use wasm_bindgen::prelude::wasm_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;
|
||||
|
||||
/// Extensions to the interpreter that are specific to the web platform.
|
||||
|
@ -55,7 +59,7 @@ mod js {
|
|||
"{this.stack.pop();}"
|
||||
}
|
||||
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) {
|
||||
"{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$));}"
|
||||
}
|
||||
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) {
|
||||
"{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;}"
|
||||
}
|
||||
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) {
|
||||
"{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) {
|
||||
"{this.nodes[$id$].textContent = $text$;}"
|
||||
|
@ -173,13 +177,13 @@ mod js {
|
|||
fn foreign_event_listener(event: &str<u8, evt>, id: u32, bubbles: u8) {
|
||||
r#"
|
||||
bubbles = bubbles == 1;
|
||||
let node = this.nodes[id];
|
||||
if(node.listening){
|
||||
node.listening += 1;
|
||||
let this_node = this.nodes[id];
|
||||
if(this_node.listening){
|
||||
this_node.listening += 1;
|
||||
} 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$;
|
||||
|
||||
// if this is a mounted listener, we send the event immediately
|
||||
|
@ -193,7 +197,7 @@ mod js {
|
|||
})
|
||||
);
|
||||
} else {
|
||||
this.listeners.create(event_name, node, bubbles, (event) => {
|
||||
this.createListener(event_name, this_node, bubbles, (event) => {
|
||||
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) {
|
||||
// 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
|
||||
.foreign_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "system",
|
||||
"module": "CommonJS",
|
||||
"lib": [
|
||||
"ES2015",
|
||||
"DOM",
|
||||
|
|
Loading…
Reference in a new issue