fix navigating when files are dropped
This commit is contained in:
23 files changed
+1242
-1026
No files matched your search
@@ -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
@@ -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:#?}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
13493771420133770074
|
4429706825984325407
|
||||||
File diff suppressed because it is too large.
Load diff
+129
-158
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -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":
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "system",
|
"module": "CommonJS",
|
||||||
"lib": [
|
"lib": [
|
||||||
"ES2015",
|
"ES2015",
|
||||||
"DOM",
|
"DOM",
|
||||||
|
|||||||
Reference in new issue
Block a user